From 55469dafb60510385fca9cd08a73bd3a78a4eddb Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Mon, 29 Jun 2026 16:42:14 +0300 Subject: [PATCH 01/15] add notification quiet window --- .../webapp/mobile/AppNotificationManager.java | 1 + .../webapp/mobile/MedicAndroidJavascript.java | 7 +++++ .../webapp/mobile/NotificationWorker.java | 26 +++++++++++++++++-- .../org/medicmobile/webapp/mobile/Utils.java | 9 +++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java index 8e204111..b086e9de 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java +++ b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java @@ -35,6 +35,7 @@ public class AppNotificationManager { private static final String CHANNEL_NAME = "CHT Android Notifications"; public static final int REQUEST_NOTIFICATION_PERMISSION = 1001; public static final String TASK_NOTIFICATIONS_KEY = "task_notifications"; + public static final String TASK_NOTIFICATION_WINDOW_KEY = "task_notification_window"; public static final String TASK_NOTIFICATION_DAY_KEY = "cht_task_notification_day"; public static final String LATEST_NOTIFICATION_TIMESTAMP_KEY = "cht_task_notification_timestamp"; public static final String MAX_NOTIFICATIONS_TO_SHOW_KEY = "cht_max_task_notifications"; diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java index 88f8453b..7d237ab9 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java +++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java @@ -169,9 +169,16 @@ public void mrdt_verify() { @JavascriptInterface public void updateTaskNotificationStore(String notifications, long maxNotifications) { + String notificationWindow = "{start:'08:00',end:'19:00'}"; + updateTaskNotificationStore(notifications, maxNotifications, notificationWindow); + } + + @JavascriptInterface + public void updateTaskNotificationStore(String notifications, long maxNotifications, String notificationWindow) { AppDataStore appDataStore = AppDataStore.getInstance(parent.getApplicationContext()); appDataStore.saveLong(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, maxNotifications); appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATIONS_KEY, notifications); + appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, notificationWindow); } @android.webkit.JavascriptInterface diff --git a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java index c56f94a7..17379705 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java +++ b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java @@ -9,8 +9,13 @@ import androidx.work.WorkerParameters; import org.json.JSONException; +import org.json.JSONObject; import org.medicmobile.webapp.mobile.util.AppDataStore; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + public class NotificationWorker extends Worker { public static final String NOTIFICATION_WORK_REQUEST_TAG = "cht_notification_tag"; public static final String NOTIFICATION_WORK_NAME = "appNotifications"; @@ -27,13 +32,30 @@ public Result doWork() { AppDataStore appDataStore = AppDataStore.getInstance(context); AppNotificationManager appNotificationManager = new AppNotificationManager(context); try { - String result = appDataStore + String notificationWindowSettings = appDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}"); + if (isNotificationWindow(notificationWindowSettings)) { + String result = appDataStore .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); - appNotificationManager.showNotificationsFromJsArray(result); + appNotificationManager.showNotificationsFromJsArray(result); + } return Result.success(); } catch (JSONException e) { log(e, "error showing notifications"); return Result.failure(); } } + + private boolean isNotificationWindow(String windowObject) throws JSONException { + JSONObject data = Utils.parseJSONObject(windowObject); + LocalTime start = formatTime(data.getString("start")); + LocalTime end = formatTime(data.getString("end")); + LocalTime now = LocalTime.now(ZoneId.systemDefault()); + return now.isAfter(start) && now.isBefore(end); + } + + private LocalTime formatTime(String timeString){ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); + return LocalTime.parse(timeString, formatter); + } } diff --git a/src/main/java/org/medicmobile/webapp/mobile/Utils.java b/src/main/java/org/medicmobile/webapp/mobile/Utils.java index daaa4df8..f3ae735d 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/Utils.java +++ b/src/main/java/org/medicmobile/webapp/mobile/Utils.java @@ -140,6 +140,15 @@ static JSONArray parseJSArrayData(String stringArray) { } } + static JSONObject parseJSONObject(String inputObject) { + try { + return new JSONObject(inputObject); + } catch (JSONException e) { + log(e, "error parsing object"); + return new JSONObject(); + } + } + static boolean checkIfDomainsAreVerified(Context context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { return true; From 5558aa2a4971b6c3b8b8684d94e713555094a5c7 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 08:47:24 +0300 Subject: [PATCH 02/15] remove defaults --- .../org/medicmobile/webapp/mobile/MedicAndroidJavascript.java | 3 +-- .../java/org/medicmobile/webapp/mobile/NotificationWorker.java | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java index 7d237ab9..d8f0b8e9 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java +++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java @@ -169,8 +169,7 @@ public void mrdt_verify() { @JavascriptInterface public void updateTaskNotificationStore(String notifications, long maxNotifications) { - String notificationWindow = "{start:'08:00',end:'19:00'}"; - updateTaskNotificationStore(notifications, maxNotifications, notificationWindow); + updateTaskNotificationStore(notifications, maxNotifications, "{}"); } @JavascriptInterface diff --git a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java index 17379705..1afd953e 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java +++ b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java @@ -48,6 +48,9 @@ public Result doWork() { private boolean isNotificationWindow(String windowObject) throws JSONException { JSONObject data = Utils.parseJSONObject(windowObject); + if (!data.has("start") || !data.has("end")) { + return true; + } LocalTime start = formatTime(data.getString("start")); LocalTime end = formatTime(data.getString("end")); LocalTime now = LocalTime.now(ZoneId.systemDefault()); From 4435c449d03dfdd80f19f8620c92a20588a39a43 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 08:53:16 +0300 Subject: [PATCH 03/15] clean up --- .../org/medicmobile/webapp/mobile/MedicAndroidJavascript.java | 1 + .../org/medicmobile/webapp/mobile/NotificationWorker.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java index d8f0b8e9..b90b5e92 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java +++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java @@ -167,6 +167,7 @@ public void mrdt_verify() { } } + //CHT-Core v5.1 and v5.2 uses this @JavascriptInterface public void updateTaskNotificationStore(String notifications, long maxNotifications) { updateTaskNotificationStore(notifications, maxNotifications, "{}"); diff --git a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java index 1afd953e..2eb0dc4c 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java +++ b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java @@ -35,9 +35,9 @@ public Result doWork() { String notificationWindowSettings = appDataStore .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}"); if (isNotificationWindow(notificationWindowSettings)) { - String result = appDataStore + String notifications = appDataStore .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); - appNotificationManager.showNotificationsFromJsArray(result); + appNotificationManager.showNotificationsFromJsArray(notifications); } return Result.success(); } catch (JSONException e) { From ae43733b1996dd2de3b694e9b2a3fa6a7983c9b1 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 08:55:19 +0300 Subject: [PATCH 04/15] clean up --- .../org/medicmobile/webapp/mobile/MedicAndroidJavascript.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java index b90b5e92..7615aafb 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java +++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java @@ -167,7 +167,7 @@ public void mrdt_verify() { } } - //CHT-Core v5.1 and v5.2 uses this + //CHT-Core v5.1 and v5.2 use this @JavascriptInterface public void updateTaskNotificationStore(String notifications, long maxNotifications) { updateTaskNotificationStore(notifications, maxNotifications, "{}"); From 882e0a8d0c261a46d3c8ccc18b79d3245c33ade5 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 09:22:19 +0300 Subject: [PATCH 05/15] clean up getstartofday --- .../medicmobile/webapp/mobile/AppNotificationManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java index b086e9de..b68859d1 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java +++ b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java @@ -150,8 +150,9 @@ public void saveLatestNotificationTimestamp(long value) { } public long getStartOfDay() { - return LocalDate.now() - .atStartOfDay(ZoneId.systemDefault()) + ZoneId zone = ZoneId.systemDefault(); + return LocalDate.now(zone) + .atStartOfDay(zone) .toInstant().toEpochMilli(); } From 5d82730be7111100ac365b440eab72096ec89351 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 11:27:25 +0300 Subject: [PATCH 06/15] add tests --- .../webapp/mobile/NotificationWorker.java | 10 +- .../webapp/mobile/NotificationWorkerTest.java | 315 ++++++++++++++++++ 2 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java diff --git a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java index 2eb0dc4c..5c75f233 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java +++ b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java @@ -53,12 +53,20 @@ private boolean isNotificationWindow(String windowObject) throws JSONException { } LocalTime start = formatTime(data.getString("start")); LocalTime end = formatTime(data.getString("end")); + if (start == null || end == null) { + return true; + } LocalTime now = LocalTime.now(ZoneId.systemDefault()); return now.isAfter(start) && now.isBefore(end); } private LocalTime formatTime(String timeString){ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); - return LocalTime.parse(timeString, formatter); + try{ + return LocalTime.parse(timeString, formatter); + } catch (Exception e) { + log(e, "Error parsing window time"); + return null; + } } } diff --git a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java new file mode 100644 index 00000000..b50b1982 --- /dev/null +++ b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java @@ -0,0 +1,315 @@ +package org.medicmobile.webapp.mobile; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.work.ListenableWorker; +import androidx.work.WorkerParameters; + +import org.json.JSONException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.medicmobile.webapp.mobile.util.AppDataStore; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +import java.lang.reflect.Method; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +@RunWith(RobolectricTestRunner.class) +public class NotificationWorkerTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private AppDataStore mockAppDataStore; + + private Context context; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + } + + @Test + public void doWork_returnsSuccess_whenNoException() throws JSONException { + String notificationWindowSettings = "{}"; // Empty = always in window + String notifications = "[]"; + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedConstruction notificationMgrMock + = mockConstruction(AppNotificationManager.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + when(mockAppDataStore.getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .thenReturn(notificationWindowSettings); + when(mockAppDataStore.getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) + .thenReturn(notifications); + + NotificationWorker worker = createWorker(); + + ListenableWorker.Result result = worker.doWork(); + + // Assert + assertEquals(ListenableWorker.Result.success(), result); + verify(notificationMgrMock.constructed().get(0), times(1)) + .showNotificationsFromJsArray(notifications); + } + } + + @Test + public void doWork_returnsFailure_whenJSONExceptionOccurs() throws JSONException { + String invalidNotificationWindowSettings = "{invalid json}"; + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedConstruction notificationMgrMock = + mockConstruction(AppNotificationManager.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .thenReturn(invalidNotificationWindowSettings); + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) + .thenReturn("[]"); + + NotificationWorker worker = createWorker(); + + ListenableWorker.Result result = worker.doWork(); + + // Invalid JSON is handled gracefully + // by Utils.parseJSONObject which returns empty JSONObject + // So this returns success and showNotifications is called + assertEquals(ListenableWorker.Result.success(), result); + verify(notificationMgrMock.constructed().get(0), times(1)) + .showNotificationsFromJsArray(anyString()); + } + } + + @Test + public void doWork_callsShowNotifications_whenInNotificationWindow() throws JSONException { + // setup + LocalTime now = LocalTime.now(ZoneId.systemDefault()); + LocalTime startTime = now.minusHours(1); + LocalTime endTime = now.plusHours(1); + String notificationWindowSettings = getWindowSettings(startTime, endTime); + String notifications = "[]"; + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedConstruction notificationMgrMock = + mockConstruction(AppNotificationManager.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .thenReturn(notificationWindowSettings); + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) + .thenReturn(notifications); + + NotificationWorker worker = createWorker(); + + ListenableWorker.Result result = worker.doWork(); + + assertEquals(ListenableWorker.Result.success(), result); + verify(notificationMgrMock.constructed().get(0), times(1)) + .showNotificationsFromJsArray(notifications); + } + } + + @Test + public void doWork_doesNotCallShowNotifications_whenOutsideNotificationWindow() throws JSONException { + LocalTime now = LocalTime.now(ZoneId.systemDefault()); + LocalTime startTime = now.minusHours(2); + LocalTime endTime = now.minusHours(1); + String notificationWindowSettings = getWindowSettings(startTime, endTime); + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedConstruction notificationMgrMock = + mockConstruction(AppNotificationManager.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .thenReturn(notificationWindowSettings); + + NotificationWorker worker = createWorker(); + + ListenableWorker.Result result = worker.doWork(); + + // Assert + assertEquals(ListenableWorker.Result.success(), result); + verify(notificationMgrMock.constructed().get(0), times(0)) + .showNotificationsFromJsArray(anyString()); + } + } + + //Apps with no/invalid window settings run all the time + @Test + public void isNotificationWindow_returnsTrue_whenBadTimeFields() throws Exception { + String windowSettings = "{\"start\": \"09:70\",\"end\": \"19:05\"}"; //bad data + + try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + + boolean result = invokeIsNotificationWindow(worker, windowSettings); + assertTrue(result); + } + } + + @Test + public void isNotificationWindow_returnsTrue_whenWithinWindow() throws Exception { + LocalTime now = LocalTime.now(ZoneId.systemDefault()); + LocalTime startTime = now.minusHours(1); + LocalTime endTime = now.plusHours(1); + String windowSettings = getWindowSettings(startTime, endTime); + + try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + + boolean result = invokeIsNotificationWindow(worker, windowSettings); + assertTrue(result); + } + } + + @Test + public void isNotificationWindow_returnsFalse_whenBeforeWindow() throws Exception { + LocalTime now = LocalTime.now(ZoneId.systemDefault()); + LocalTime startTime = now.plusHours(1); + LocalTime endTime = now.plusHours(2); + String windowSettings = getWindowSettings(startTime, endTime); + + try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + + boolean result = invokeIsNotificationWindow(worker, windowSettings); + + // Assert + assertFalse(result); + } + } + + @Test + public void isNotificationWindow_returnsFalse_whenAfterWindow() throws Exception { + LocalTime now = LocalTime.now(ZoneId.systemDefault()); + LocalTime startTime = now.minusHours(2); + LocalTime endTime = now.minusHours(1); + String windowSettings = getWindowSettings(startTime, endTime); + + try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + + boolean result = invokeIsNotificationWindow(worker, windowSettings); + + // Assert + assertFalse(result); + } + } + + @Test + public void formatTime_returnsNullForBadTime() throws Exception { + try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + LocalTime result = invokeFormatTime(worker, "14:70"); + assertNull(result); + } + } + + @Test + public void formatTime_parsesValidTimeFormats() throws Exception { + try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + + String[] testTimes = {"00:00", "12:00", "23:59"}; + LocalTime[] expectedTimes = { + LocalTime.of(0, 0), + LocalTime.of(12, 0), + LocalTime.of(23, 59) + }; + + // Act & Assert + for (int i = 0; i < testTimes.length; i++) { + LocalTime result = invokeFormatTime(worker, testTimes[i]); + assertEquals(expectedTimes[i], result); + } + } + } + + private NotificationWorker createWorker() { + WorkerParameters workerParameters = mock(WorkerParameters.class); + return new NotificationWorker(context, workerParameters); + } + + private boolean invokeIsNotificationWindow(NotificationWorker worker, String windowObject) throws Exception { + Method method = NotificationWorker.class + .getDeclaredMethod("isNotificationWindow", String.class); + method.setAccessible(true); + return (Boolean) method.invoke(worker, windowObject); + } + + private LocalTime invokeFormatTime(NotificationWorker worker, String timeString) throws Exception { + Method method = NotificationWorker.class + .getDeclaredMethod("formatTime", String.class); + method.setAccessible(true); + return (LocalTime) method.invoke(worker, timeString); + } + + private String getWindowSettings(LocalTime startTime, LocalTime endTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); + return String.format("{\"start\": \"%s\", \"end\": \"%s\"}", + startTime.format(formatter), endTime.format(formatter)); + } +} From 74b8c9b20e832a984a51b6cabd97cefa69faa66c Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 11:30:48 +0300 Subject: [PATCH 07/15] clesn up --- .../org/medicmobile/webapp/mobile/NotificationWorkerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java index b50b1982..00b99257 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java @@ -280,7 +280,7 @@ public void formatTime_parsesValidTimeFormats() throws Exception { LocalTime.of(23, 59) }; - // Act & Assert + //Assert for (int i = 0; i < testTimes.length; i++) { LocalTime result = invokeFormatTime(worker, testTimes[i]); assertEquals(expectedTimes[i], result); From 1354bfde80fbf733ca38521d539f91be321d0c03 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 12:08:21 +0300 Subject: [PATCH 08/15] fix tests --- .../webapp/mobile/NotificationWorker.java | 17 +-- .../org/medicmobile/webapp/mobile/Utils.java | 28 +++- .../webapp/mobile/NotificationWorkerTest.java | 134 ++++++------------ .../medicmobile/webapp/mobile/UtilsTest.java | 34 ++++- 4 files changed, 94 insertions(+), 119 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java index 5c75f233..a3eb6435 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java +++ b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java @@ -14,7 +14,6 @@ import java.time.LocalTime; import java.time.ZoneId; -import java.time.format.DateTimeFormatter; public class NotificationWorker extends Worker { public static final String NOTIFICATION_WORK_REQUEST_TAG = "cht_notification_tag"; @@ -46,27 +45,17 @@ public Result doWork() { } } - private boolean isNotificationWindow(String windowObject) throws JSONException { + boolean isNotificationWindow(String windowObject) throws JSONException { JSONObject data = Utils.parseJSONObject(windowObject); if (!data.has("start") || !data.has("end")) { return true; } - LocalTime start = formatTime(data.getString("start")); - LocalTime end = formatTime(data.getString("end")); + LocalTime start = Utils.formatTime(data.getString("start")); + LocalTime end = Utils.formatTime(data.getString("end")); if (start == null || end == null) { return true; } LocalTime now = LocalTime.now(ZoneId.systemDefault()); return now.isAfter(start) && now.isBefore(end); } - - private LocalTime formatTime(String timeString){ - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); - try{ - return LocalTime.parse(timeString, formatter); - } catch (Exception e) { - log(e, "Error parsing window time"); - return null; - } - } } diff --git a/src/main/java/org/medicmobile/webapp/mobile/Utils.java b/src/main/java/org/medicmobile/webapp/mobile/Utils.java index f3ae735d..acbbe136 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/Utils.java +++ b/src/main/java/org/medicmobile/webapp/mobile/Utils.java @@ -20,11 +20,14 @@ import org.json.JSONObject; import java.io.File; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.Optional; final class Utils { - private Utils() {} + private Utils() { + } /** * @see #isValidNavigationUrl(String, String) @@ -64,10 +67,10 @@ static boolean isValidNavigationUrl(String appUrl, String navUrl) { } static JSONObject json(Object... keyVals) throws JSONException { - if(DEBUG && keyVals.length % 2 != 0) throw new AssertionError(); + if (DEBUG && keyVals.length % 2 != 0) throw new AssertionError(); JSONObject o = new JSONObject(); - for(int i=keyVals.length-1; i>0; i-=2) { - o.put(keyVals[i-1].toString(), keyVals[i]); + for (int i = keyVals.length - 1; i > 0; i -= 2) { + o.put(keyVals[i - 1].toString(), keyVals[i]); } return o; } @@ -77,7 +80,7 @@ static boolean intentHandlerAvailableFor(Context ctx, Intent intent) { } static void startAppActivityChain(Activity a) { - if(SettingsStore.in(a).hasWebappSettings()) { + if (SettingsStore.in(a).hasWebappSettings()) { a.startActivity(new Intent(a, EmbeddedBrowserActivity.class)); } else { a.startActivity(new Intent(a, SettingsDialogActivity.class)); @@ -86,7 +89,7 @@ static void startAppActivityChain(Activity a) { } static String createUseragentFrom(String current) { - if(current.contains(APPLICATION_ID)) return current; + if (current.contains(APPLICATION_ID)) return current; return String.format("%s %s/%s", current, APPLICATION_ID, VERSION_NAME); @@ -101,6 +104,7 @@ static void restartApp(Context context) { /** * The file path can be a regular file or a content ("content://" scheme) + * * @param path {String} File path * @return {Uri} */ @@ -128,6 +132,7 @@ static boolean isDebug() { /** * parses js string array to JSONArray + * * @param stringArray {String} array in string format * @return {JSONArray} */ @@ -149,6 +154,17 @@ static JSONObject parseJSONObject(String inputObject) { } } + //formats string time to HH:mm + static LocalTime formatTime(String timeString) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); + try { + return LocalTime.parse(timeString, formatter); + } catch (Exception e) { + log(e, "Error parsing window time"); + return null; + } + } + static boolean checkIfDomainsAreVerified(Context context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { return true; diff --git a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java index 00b99257..26998245 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; @@ -31,7 +30,6 @@ import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; -import java.lang.reflect.Method; import java.time.LocalTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -54,20 +52,19 @@ public void setUp() { @Test public void doWork_returnsSuccess_whenNoException() throws JSONException { - String notificationWindowSettings = "{}"; // Empty = always in window + String notificationWindowSettings = "{}"; // Empty = always in window String notifications = "[]"; try (MockedStatic dataMock = mockStatic(AppDataStore.class); - MockedConstruction notificationMgrMock - = mockConstruction(AppNotificationManager.class)) { - + MockedConstruction notificationMgrMock = mockConstruction( + AppNotificationManager.class)) { dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); + .thenReturn(mockAppDataStore); when(mockAppDataStore.getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) - .thenReturn(notificationWindowSettings); + .thenReturn(notificationWindowSettings); when(mockAppDataStore.getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) - .thenReturn(notifications); + .thenReturn(notifications); NotificationWorker worker = createWorker(); @@ -76,7 +73,7 @@ public void doWork_returnsSuccess_whenNoException() throws JSONException { // Assert assertEquals(ListenableWorker.Result.success(), result); verify(notificationMgrMock.constructed().get(0), times(1)) - .showNotificationsFromJsArray(notifications); + .showNotificationsFromJsArray(notifications); } } @@ -85,18 +82,18 @@ public void doWork_returnsFailure_whenJSONExceptionOccurs() throws JSONException String invalidNotificationWindowSettings = "{invalid json}"; try (MockedStatic dataMock = mockStatic(AppDataStore.class); - MockedConstruction notificationMgrMock = - mockConstruction(AppNotificationManager.class)) { + MockedConstruction notificationMgrMock = mockConstruction( + AppNotificationManager.class)) { dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); + .thenReturn(mockAppDataStore); when(mockAppDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) - .thenReturn(invalidNotificationWindowSettings); + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .thenReturn(invalidNotificationWindowSettings); when(mockAppDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) - .thenReturn("[]"); + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) + .thenReturn("[]"); NotificationWorker worker = createWorker(); @@ -107,7 +104,7 @@ public void doWork_returnsFailure_whenJSONExceptionOccurs() throws JSONException // So this returns success and showNotifications is called assertEquals(ListenableWorker.Result.success(), result); verify(notificationMgrMock.constructed().get(0), times(1)) - .showNotificationsFromJsArray(anyString()); + .showNotificationsFromJsArray(anyString()); } } @@ -121,18 +118,18 @@ public void doWork_callsShowNotifications_whenInNotificationWindow() throws JSON String notifications = "[]"; try (MockedStatic dataMock = mockStatic(AppDataStore.class); - MockedConstruction notificationMgrMock = - mockConstruction(AppNotificationManager.class)) { + MockedConstruction notificationMgrMock = mockConstruction( + AppNotificationManager.class)) { dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); + .thenReturn(mockAppDataStore); when(mockAppDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) - .thenReturn(notificationWindowSettings); + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .thenReturn(notificationWindowSettings); when(mockAppDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) - .thenReturn(notifications); + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) + .thenReturn(notifications); NotificationWorker worker = createWorker(); @@ -140,7 +137,7 @@ public void doWork_callsShowNotifications_whenInNotificationWindow() throws JSON assertEquals(ListenableWorker.Result.success(), result); verify(notificationMgrMock.constructed().get(0), times(1)) - .showNotificationsFromJsArray(notifications); + .showNotificationsFromJsArray(notifications); } } @@ -152,15 +149,15 @@ public void doWork_doesNotCallShowNotifications_whenOutsideNotificationWindow() String notificationWindowSettings = getWindowSettings(startTime, endTime); try (MockedStatic dataMock = mockStatic(AppDataStore.class); - MockedConstruction notificationMgrMock = - mockConstruction(AppNotificationManager.class)) { + MockedConstruction notificationMgrMock = mockConstruction( + AppNotificationManager.class)) { dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); + .thenReturn(mockAppDataStore); when(mockAppDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) - .thenReturn(notificationWindowSettings); + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .thenReturn(notificationWindowSettings); NotificationWorker worker = createWorker(); @@ -169,23 +166,23 @@ public void doWork_doesNotCallShowNotifications_whenOutsideNotificationWindow() // Assert assertEquals(ListenableWorker.Result.success(), result); verify(notificationMgrMock.constructed().get(0), times(0)) - .showNotificationsFromJsArray(anyString()); + .showNotificationsFromJsArray(anyString()); } } - //Apps with no/invalid window settings run all the time + // Apps with no/invalid window settings run all the time @Test public void isNotificationWindow_returnsTrue_whenBadTimeFields() throws Exception { - String windowSettings = "{\"start\": \"09:70\",\"end\": \"19:05\"}"; //bad data + String windowSettings = "{\"start\": \"09:70\",\"end\": \"19:05\"}"; // bad data try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); + .thenReturn(mockAppDataStore); NotificationWorker worker = createWorker(); - boolean result = invokeIsNotificationWindow(worker, windowSettings); + boolean result = worker.isNotificationWindow(windowSettings); assertTrue(result); } } @@ -200,11 +197,11 @@ public void isNotificationWindow_returnsTrue_whenWithinWindow() throws Exception try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); + .thenReturn(mockAppDataStore); NotificationWorker worker = createWorker(); - boolean result = invokeIsNotificationWindow(worker, windowSettings); + boolean result = worker.isNotificationWindow(windowSettings); assertTrue(result); } } @@ -219,11 +216,11 @@ public void isNotificationWindow_returnsFalse_whenBeforeWindow() throws Exceptio try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); + .thenReturn(mockAppDataStore); NotificationWorker worker = createWorker(); - boolean result = invokeIsNotificationWindow(worker, windowSettings); + boolean result = worker.isNotificationWindow(windowSettings); // Assert assertFalse(result); @@ -240,76 +237,25 @@ public void isNotificationWindow_returnsFalse_whenAfterWindow() throws Exception try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); + .thenReturn(mockAppDataStore); NotificationWorker worker = createWorker(); - boolean result = invokeIsNotificationWindow(worker, windowSettings); + boolean result = worker.isNotificationWindow(windowSettings); // Assert assertFalse(result); } } - @Test - public void formatTime_returnsNullForBadTime() throws Exception { - try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { - - dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); - - NotificationWorker worker = createWorker(); - LocalTime result = invokeFormatTime(worker, "14:70"); - assertNull(result); - } - } - - @Test - public void formatTime_parsesValidTimeFormats() throws Exception { - try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { - - dataMock.when(() -> AppDataStore.getInstance(context)) - .thenReturn(mockAppDataStore); - - NotificationWorker worker = createWorker(); - - String[] testTimes = {"00:00", "12:00", "23:59"}; - LocalTime[] expectedTimes = { - LocalTime.of(0, 0), - LocalTime.of(12, 0), - LocalTime.of(23, 59) - }; - - //Assert - for (int i = 0; i < testTimes.length; i++) { - LocalTime result = invokeFormatTime(worker, testTimes[i]); - assertEquals(expectedTimes[i], result); - } - } - } - private NotificationWorker createWorker() { WorkerParameters workerParameters = mock(WorkerParameters.class); return new NotificationWorker(context, workerParameters); } - private boolean invokeIsNotificationWindow(NotificationWorker worker, String windowObject) throws Exception { - Method method = NotificationWorker.class - .getDeclaredMethod("isNotificationWindow", String.class); - method.setAccessible(true); - return (Boolean) method.invoke(worker, windowObject); - } - - private LocalTime invokeFormatTime(NotificationWorker worker, String timeString) throws Exception { - Method method = NotificationWorker.class - .getDeclaredMethod("formatTime", String.class); - method.setAccessible(true); - return (LocalTime) method.invoke(worker, timeString); - } - private String getWindowSettings(LocalTime startTime, LocalTime endTime) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); return String.format("{\"start\": \"%s\", \"end\": \"%s\"}", - startTime.format(formatter), endTime.format(formatter)); + startTime.format(formatter), endTime.format(formatter)); } } diff --git a/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java b/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java index efe3ddf1..a6801961 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -22,6 +23,7 @@ import java.io.File; import java.io.IOException; +import java.time.LocalTime; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -38,7 +40,7 @@ public void isUrlRelated_goodNormalUrls() { "https://example.com/medic/_design/medic/_rewrite", }; - for(String goodUrl : goodUrls) { + for (String goodUrl : goodUrls) { assertTrue("Expected URL to be accepted, but it wasn't: " + goodUrl, Utils.isUrlRelated("https://example.com", Uri.parse(goodUrl))); } @@ -50,7 +52,7 @@ public void isUrlRelated_goodBlobs() { "blob:https://example.com/medic/_design/medic/_rewrite", }; - for(String goodBlobUrl : goodBlobUrls) { + for (String goodBlobUrl : goodBlobUrls) { assertTrue("Expected URL to be accepted, but it wasn't: " + goodBlobUrl, Utils.isUrlRelated("https://example.com", Uri.parse(goodBlobUrl))); } @@ -66,7 +68,7 @@ public void isUrlRelated_badUrls() { "sms:0040733898569,0040788963214&body=Thisisthesmsbody", }; - for(String badUrl : badUrls) { + for (String badUrl : badUrls) { assertFalse("Expected URL to be rejected, but it wasn't: " + badUrl, Utils.isUrlRelated("https://example.com", Uri.parse(badUrl))); } @@ -121,7 +123,7 @@ public void validNavigationUrls() { "blob:https://gamma-cht.dev.medicmobile.org/#/reports" }; - for(String goodBlobUrl : goodBlobUrls) { + for (String goodBlobUrl : goodBlobUrls) { assertTrue("Expected URL to be accepted, but it wasn't: " + goodBlobUrl, Utils.isValidNavigationUrl("https://gamma-cht.dev.medicmobile.org", goodBlobUrl)); } @@ -136,7 +138,7 @@ public void nullUrlsNotValid() { {"", ""}, }; - for(String[] nullUrlPair : nullUrls) { + for (String[] nullUrlPair : nullUrls) { assertFalse("Not expected URLs to be accepted, but they were: " + nullUrlPair[0] + " , " + nullUrlPair[1], Utils.isValidNavigationUrl(nullUrlPair[0], nullUrlPair[1])); } @@ -268,4 +270,26 @@ public void testPackageManagerException() throws PackageManager.NameNotFoundExce )); } } + + @Test + public void formatTime_returnsNullForBadTime() throws Exception { + LocalTime result = Utils.formatTime("14:70"); + assertNull(result); + } + + @Test + public void formatTime_parsesValidTimeFormats() throws Exception { + String[] testTimes = {"00:00", "12:00", "23:59"}; + LocalTime[] expectedTimes = { + LocalTime.of(0, 0), + LocalTime.of(12, 0), + LocalTime.of(23, 59) + }; + + //Assert + for (int i = 0; i < testTimes.length; i++) { + LocalTime result = Utils.formatTime(testTimes[i]); + assertEquals(expectedTimes[i], result); + } + } } From 10ce752e722d66bfd449b5129220d9d7879328df Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 12:24:49 +0300 Subject: [PATCH 09/15] improve bad time logic --- .../org/medicmobile/webapp/mobile/NotificationWorker.java | 6 +++--- .../medicmobile/webapp/mobile/NotificationWorkerTest.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java index a3eb6435..64a5872c 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java +++ b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java @@ -32,10 +32,10 @@ public Result doWork() { AppNotificationManager appNotificationManager = new AppNotificationManager(context); try { String notificationWindowSettings = appDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}"); + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}"); if (isNotificationWindow(notificationWindowSettings)) { String notifications = appDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); appNotificationManager.showNotificationsFromJsArray(notifications); } return Result.success(); @@ -53,7 +53,7 @@ boolean isNotificationWindow(String windowObject) throws JSONException { LocalTime start = Utils.formatTime(data.getString("start")); LocalTime end = Utils.formatTime(data.getString("end")); if (start == null || end == null) { - return true; + return false; } LocalTime now = LocalTime.now(ZoneId.systemDefault()); return now.isAfter(start) && now.isBefore(end); diff --git a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java index 26998245..0e2680c8 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java @@ -170,9 +170,9 @@ public void doWork_doesNotCallShowNotifications_whenOutsideNotificationWindow() } } - // Apps with no/invalid window settings run all the time + // Apps with invalid window settings time don't run @Test - public void isNotificationWindow_returnsTrue_whenBadTimeFields() throws Exception { + public void isNotificationWindow_returnsFalse_whenBadTimeFields() throws Exception { String windowSettings = "{\"start\": \"09:70\",\"end\": \"19:05\"}"; // bad data try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { @@ -183,7 +183,7 @@ public void isNotificationWindow_returnsTrue_whenBadTimeFields() throws Exceptio NotificationWorker worker = createWorker(); boolean result = worker.isNotificationWindow(windowSettings); - assertTrue(result); + assertFalse(result); } } From 05647a7bc0e5334375870ef017267f7708f7ef98 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 14:38:36 +0300 Subject: [PATCH 10/15] clean up algin with cht-core --- .../medicmobile/webapp/mobile/AppNotificationManager.java | 2 +- .../medicmobile/webapp/mobile/MedicAndroidJavascript.java | 7 ++++--- .../org/medicmobile/webapp/mobile/NotificationWorker.java | 2 +- .../medicmobile/webapp/mobile/NotificationWorkerTest.java | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java index b68859d1..ca598fa6 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java +++ b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java @@ -35,7 +35,7 @@ public class AppNotificationManager { private static final String CHANNEL_NAME = "CHT Android Notifications"; public static final int REQUEST_NOTIFICATION_PERMISSION = 1001; public static final String TASK_NOTIFICATIONS_KEY = "task_notifications"; - public static final String TASK_NOTIFICATION_WINDOW_KEY = "task_notification_window"; + public static final String TASK_NOTIFICATION_SETTINGS_KEY = "task_notification_settings"; public static final String TASK_NOTIFICATION_DAY_KEY = "cht_task_notification_day"; public static final String LATEST_NOTIFICATION_TIMESTAMP_KEY = "cht_task_notification_timestamp"; public static final String MAX_NOTIFICATIONS_TO_SHOW_KEY = "cht_max_task_notifications"; diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java index 7615aafb..0a2429e5 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java +++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java @@ -170,15 +170,16 @@ public void mrdt_verify() { //CHT-Core v5.1 and v5.2 use this @JavascriptInterface public void updateTaskNotificationStore(String notifications, long maxNotifications) { - updateTaskNotificationStore(notifications, maxNotifications, "{}"); + updateTaskNotificationStoreWithSettings(notifications, maxNotifications, "{}"); } @JavascriptInterface - public void updateTaskNotificationStore(String notifications, long maxNotifications, String notificationWindow) { + public void updateTaskNotificationStoreWithSettings( + String notifications, long maxNotifications, String settings) { AppDataStore appDataStore = AppDataStore.getInstance(parent.getApplicationContext()); appDataStore.saveLong(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, maxNotifications); appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATIONS_KEY, notifications); - appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, notificationWindow); + appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, settings); } @android.webkit.JavascriptInterface diff --git a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java index 64a5872c..4af0f64b 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java +++ b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java @@ -32,7 +32,7 @@ public Result doWork() { AppNotificationManager appNotificationManager = new AppNotificationManager(context); try { String notificationWindowSettings = appDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}"); + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}"); if (isNotificationWindow(notificationWindowSettings)) { String notifications = appDataStore .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); diff --git a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java index 0e2680c8..13496762 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java @@ -61,7 +61,7 @@ public void doWork_returnsSuccess_whenNoException() throws JSONException { dataMock.when(() -> AppDataStore.getInstance(context)) .thenReturn(mockAppDataStore); - when(mockAppDataStore.getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + when(mockAppDataStore.getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}")) .thenReturn(notificationWindowSettings); when(mockAppDataStore.getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) .thenReturn(notifications); @@ -89,7 +89,7 @@ public void doWork_returnsFailure_whenJSONExceptionOccurs() throws JSONException .thenReturn(mockAppDataStore); when(mockAppDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}")) .thenReturn(invalidNotificationWindowSettings); when(mockAppDataStore .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) @@ -125,7 +125,7 @@ public void doWork_callsShowNotifications_whenInNotificationWindow() throws JSON .thenReturn(mockAppDataStore); when(mockAppDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}")) .thenReturn(notificationWindowSettings); when(mockAppDataStore .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) @@ -156,7 +156,7 @@ public void doWork_doesNotCallShowNotifications_whenOutsideNotificationWindow() .thenReturn(mockAppDataStore); when(mockAppDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_WINDOW_KEY, "{}")) + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}")) .thenReturn(notificationWindowSettings); NotificationWorker worker = createWorker(); From b93ba729b3a2dd03540f0201dda7c4bc30d98694 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Tue, 30 Jun 2026 22:53:11 +0300 Subject: [PATCH 11/15] save notification data in one atomic transaction --- .../webapp/mobile/MedicAndroidJavascript.java | 7 ++-- .../webapp/mobile/util/AppDataStore.java | 39 ++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java index 0a2429e5..9c980026 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java +++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java @@ -177,9 +177,10 @@ public void updateTaskNotificationStore(String notifications, long maxNotificati public void updateTaskNotificationStoreWithSettings( String notifications, long maxNotifications, String settings) { AppDataStore appDataStore = AppDataStore.getInstance(parent.getApplicationContext()); - appDataStore.saveLong(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, maxNotifications); - appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATIONS_KEY, notifications); - appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, settings); + appDataStore.saveTaskNotificationSettingsBlocking( + AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, settings, + AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, maxNotifications, + AppNotificationManager.TASK_NOTIFICATIONS_KEY, notifications); } @android.webkit.JavascriptInterface diff --git a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java index eeddc2b9..e7c3c70f 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java +++ b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java @@ -16,9 +16,12 @@ import io.reactivex.rxjava3.core.Single; +import java.util.concurrent.TimeUnit; + @OptIn(markerClass = kotlinx.coroutines.ExperimentalCoroutinesApi.class) public class AppDataStore { private static final String DATASTORE_NAME = "cht_datastore"; + private static final long WRITE_TIMEOUT_SECONDS = 5; private static AppDataStore instance; private final RxDataStore dataStore; @@ -55,9 +58,41 @@ public void saveLong(String key, Long value) { save(PreferencesKeys.longKey(key), value); } + private void saveBlocking(Key prefKey, T value) { + try { + Preferences ignored = save(prefKey, value) + .timeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .blockingGet(); // NOSONAR + } catch (Exception e) { + log(e, "AppDataStore :: blocking save failed/timed out for key %s", prefKey); + } + } + public void saveLongBlocking(String key, Long value) { - Single updateResult = save(PreferencesKeys.longKey(key), value); - Preferences ignored = updateResult.blockingGet(); // NOSONAR + saveBlocking(PreferencesKeys.longKey(key), value); + } + + /** + * Persists the task-notifications, settings and max-count in a single atomic transaction. + */ + public void saveTaskNotificationSettingsBlocking( + String settingsKey, String settings, + String maxKey, long max, + String notificationsKey, String notifications) { + try { + Preferences ignored = dataStore + .updateDataAsync(preferences -> { + MutablePreferences mutablePreferences = preferences.toMutablePreferences(); + mutablePreferences.set(PreferencesKeys.stringKey(settingsKey), settings); + mutablePreferences.set(PreferencesKeys.longKey(maxKey), max); + mutablePreferences.set(PreferencesKeys.stringKey(notificationsKey), notifications); + return Single.just(mutablePreferences); + }) + .timeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .blockingGet(); // NOSONAR + } catch (Exception e) { + log(e, "AppDataStore :: saving task notification settings failed/timed out"); + } } private T getBlocking(Key key, @Nullable T defaultValue) { From 14d2358db14aaf2124f1e5580c29c90c22798918 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Wed, 1 Jul 2026 08:39:29 +0300 Subject: [PATCH 12/15] clean up notification trans --- .../webapp/mobile/MedicAndroidJavascript.java | 5 +- .../webapp/mobile/util/AppDataStore.java | 66 +++++++++---------- .../webapp/mobile/NotificationWorkerTest.java | 48 +++++++------- 3 files changed, 56 insertions(+), 63 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java index 9c980026..0f10808f 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java +++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java @@ -177,10 +177,7 @@ public void updateTaskNotificationStore(String notifications, long maxNotificati public void updateTaskNotificationStoreWithSettings( String notifications, long maxNotifications, String settings) { AppDataStore appDataStore = AppDataStore.getInstance(parent.getApplicationContext()); - appDataStore.saveTaskNotificationSettingsBlocking( - AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, settings, - AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, maxNotifications, - AppNotificationManager.TASK_NOTIFICATIONS_KEY, notifications); + appDataStore.saveTaskNotificationSettingsBlocking(settings, maxNotifications, notifications); } @android.webkit.JavascriptInterface diff --git a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java index e7c3c70f..1b0b5847 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java +++ b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java @@ -14,10 +14,12 @@ import androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder; import androidx.datastore.rxjava3.RxDataStore; -import io.reactivex.rxjava3.core.Single; +import org.medicmobile.webapp.mobile.AppNotificationManager; import java.util.concurrent.TimeUnit; +import io.reactivex.rxjava3.core.Single; + @OptIn(markerClass = kotlinx.coroutines.ExperimentalCoroutinesApi.class) public class AppDataStore { private static final String DATASTORE_NAME = "cht_datastore"; @@ -58,41 +60,9 @@ public void saveLong(String key, Long value) { save(PreferencesKeys.longKey(key), value); } - private void saveBlocking(Key prefKey, T value) { - try { - Preferences ignored = save(prefKey, value) - .timeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .blockingGet(); // NOSONAR - } catch (Exception e) { - log(e, "AppDataStore :: blocking save failed/timed out for key %s", prefKey); - } - } - public void saveLongBlocking(String key, Long value) { - saveBlocking(PreferencesKeys.longKey(key), value); - } - - /** - * Persists the task-notifications, settings and max-count in a single atomic transaction. - */ - public void saveTaskNotificationSettingsBlocking( - String settingsKey, String settings, - String maxKey, long max, - String notificationsKey, String notifications) { - try { - Preferences ignored = dataStore - .updateDataAsync(preferences -> { - MutablePreferences mutablePreferences = preferences.toMutablePreferences(); - mutablePreferences.set(PreferencesKeys.stringKey(settingsKey), settings); - mutablePreferences.set(PreferencesKeys.longKey(maxKey), max); - mutablePreferences.set(PreferencesKeys.stringKey(notificationsKey), notifications); - return Single.just(mutablePreferences); - }) - .timeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .blockingGet(); // NOSONAR - } catch (Exception e) { - log(e, "AppDataStore :: saving task notification settings failed/timed out"); - } + Single updateResult = save(PreferencesKeys.longKey(key), value); + Preferences ignored = updateResult.blockingGet(); // NOSONAR } private T getBlocking(Key key, @Nullable T defaultValue) { @@ -112,4 +82,30 @@ public String getStringBlocking(String key, @Nullable String defaultValue) { public long getLongBlocking(String key, @Nullable Long defaultValue) { return getBlocking(PreferencesKeys.longKey(key), defaultValue); } + + /** + * Persists the task-notifications, settings and max-count in a single atomic transaction. + */ + public void saveTaskNotificationSettingsBlocking( + String settings, + long max, + String notifications) { + try { + Preferences ignored = dataStore + .updateDataAsync(preferences -> { + MutablePreferences mutablePreferences = preferences.toMutablePreferences(); + mutablePreferences.set(PreferencesKeys + .stringKey(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY), settings); + mutablePreferences.set(PreferencesKeys + .longKey(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY), max); + mutablePreferences.set(PreferencesKeys + .stringKey(AppNotificationManager.TASK_NOTIFICATIONS_KEY), notifications); + return Single.just(mutablePreferences); + }) + .timeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .blockingGet(); // NOSONAR + } catch (Exception e) { + log(e, "AppDataStore :: saving task notification settings failed/timed out"); + } + } } diff --git a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java index 13496762..0987357c 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java @@ -3,6 +3,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; @@ -36,6 +38,7 @@ @RunWith(RobolectricTestRunner.class) public class NotificationWorkerTest { + private static final LocalTime FIXED_NOW = LocalTime.of(12, 0); @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @@ -110,14 +113,11 @@ public void doWork_returnsFailure_whenJSONExceptionOccurs() throws JSONException @Test public void doWork_callsShowNotifications_whenInNotificationWindow() throws JSONException { - // setup - LocalTime now = LocalTime.now(ZoneId.systemDefault()); - LocalTime startTime = now.minusHours(1); - LocalTime endTime = now.plusHours(1); - String notificationWindowSettings = getWindowSettings(startTime, endTime); + String notificationWindowSettings = getWindowSettings(FIXED_NOW.minusHours(1), FIXED_NOW.plusHours(1)); String notifications = "[]"; try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW); MockedConstruction notificationMgrMock = mockConstruction( AppNotificationManager.class)) { @@ -143,12 +143,11 @@ public void doWork_callsShowNotifications_whenInNotificationWindow() throws JSON @Test public void doWork_doesNotCallShowNotifications_whenOutsideNotificationWindow() throws JSONException { - LocalTime now = LocalTime.now(ZoneId.systemDefault()); - LocalTime startTime = now.minusHours(2); - LocalTime endTime = now.minusHours(1); - String notificationWindowSettings = getWindowSettings(startTime, endTime); + // window earlier in the same day, "now" is past it + String notificationWindowSettings = getWindowSettings(FIXED_NOW.minusHours(2), FIXED_NOW.minusHours(1)); try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW); MockedConstruction notificationMgrMock = mockConstruction( AppNotificationManager.class)) { @@ -189,12 +188,10 @@ public void isNotificationWindow_returnsFalse_whenBadTimeFields() throws Excepti @Test public void isNotificationWindow_returnsTrue_whenWithinWindow() throws Exception { - LocalTime now = LocalTime.now(ZoneId.systemDefault()); - LocalTime startTime = now.minusHours(1); - LocalTime endTime = now.plusHours(1); - String windowSettings = getWindowSettings(startTime, endTime); + String windowSettings = getWindowSettings(FIXED_NOW.minusHours(1), FIXED_NOW.plusHours(1)); - try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW)) { dataMock.when(() -> AppDataStore.getInstance(context)) .thenReturn(mockAppDataStore); @@ -208,12 +205,10 @@ public void isNotificationWindow_returnsTrue_whenWithinWindow() throws Exception @Test public void isNotificationWindow_returnsFalse_whenBeforeWindow() throws Exception { - LocalTime now = LocalTime.now(ZoneId.systemDefault()); - LocalTime startTime = now.plusHours(1); - LocalTime endTime = now.plusHours(2); - String windowSettings = getWindowSettings(startTime, endTime); + String windowSettings = getWindowSettings(FIXED_NOW.plusHours(1), FIXED_NOW.plusHours(2)); - try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW)) { dataMock.when(() -> AppDataStore.getInstance(context)) .thenReturn(mockAppDataStore); @@ -229,12 +224,10 @@ public void isNotificationWindow_returnsFalse_whenBeforeWindow() throws Exceptio @Test public void isNotificationWindow_returnsFalse_whenAfterWindow() throws Exception { - LocalTime now = LocalTime.now(ZoneId.systemDefault()); - LocalTime startTime = now.minusHours(2); - LocalTime endTime = now.minusHours(1); - String windowSettings = getWindowSettings(startTime, endTime); + String windowSettings = getWindowSettings(FIXED_NOW.minusHours(2), FIXED_NOW.minusHours(1)); - try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW)) { dataMock.when(() -> AppDataStore.getInstance(context)) .thenReturn(mockAppDataStore); @@ -258,4 +251,11 @@ private String getWindowSettings(LocalTime startTime, LocalTime endTime) { return String.format("{\"start\": \"%s\", \"end\": \"%s\"}", startTime.format(formatter), endTime.format(formatter)); } + + // Pins LocalTime.now(...) to passed arg + private MockedStatic mockNow(LocalTime now) { + MockedStatic timeMock = mockStatic(LocalTime.class, CALLS_REAL_METHODS); + timeMock.when(() -> LocalTime.now(any(ZoneId.class))).thenReturn(now); + return timeMock; + } } From 1bb2a0f7e43c6a604e0aa6af3f22a8c08ae91d8a Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Wed, 1 Jul 2026 10:01:48 +0300 Subject: [PATCH 13/15] introduce settings object --- .../webapp/mobile/AppNotificationManager.java | 8 ++++++-- .../webapp/mobile/MedicAndroidJavascript.java | 7 ++++--- .../webapp/mobile/NotificationWorker.java | 12 +++++------- .../medicmobile/webapp/mobile/util/AppDataStore.java | 12 ++++++------ .../webapp/mobile/AppNotificationManagerTest.java | 7 ++++--- .../webapp/mobile/NotificationWorkerTest.java | 8 ++++---- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java index ca598fa6..6ce50119 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java +++ b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java @@ -38,7 +38,6 @@ public class AppNotificationManager { public static final String TASK_NOTIFICATION_SETTINGS_KEY = "task_notification_settings"; public static final String TASK_NOTIFICATION_DAY_KEY = "cht_task_notification_day"; public static final String LATEST_NOTIFICATION_TIMESTAMP_KEY = "cht_task_notification_timestamp"; - public static final String MAX_NOTIFICATIONS_TO_SHOW_KEY = "cht_max_task_notifications"; private final Context context; private final NotificationManager manager; @@ -115,7 +114,7 @@ public void showNotificationsFromJsArray(String jsArrayString) throws JSONExcept private void showMultipleTaskNotifications(JSONArray dataArray) throws JSONException { Intent intent = new Intent(context, EmbeddedBrowserActivity.class); intent.setData(Uri.parse(TextUtils.concat(appUrl, "/#/tasks").toString())); - long maxNotifications = appDataStore.getLongBlocking(MAX_NOTIFICATIONS_TO_SHOW_KEY, 8L); + long maxNotifications = getSettings(appDataStore).getLong("maxNotifications"); long latestStoredTimestamp = getLatestStoredTimestamp(getStartOfDay()); int counter = 0; long latestReadyAt = 0; @@ -149,6 +148,11 @@ public void saveLatestNotificationTimestamp(long value) { appDataStore.saveLong(LATEST_NOTIFICATION_TIMESTAMP_KEY, value); } + public static JSONObject getSettings(AppDataStore dataStore){ + String settings = dataStore.getStringBlocking(TASK_NOTIFICATION_SETTINGS_KEY, "{}"); + return Utils.parseJSONObject(settings); + } + public long getStartOfDay() { ZoneId zone = ZoneId.systemDefault(); return LocalDate.now(zone) diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java index 0f10808f..56d847f2 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java +++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java @@ -170,14 +170,15 @@ public void mrdt_verify() { //CHT-Core v5.1 and v5.2 use this @JavascriptInterface public void updateTaskNotificationStore(String notifications, long maxNotifications) { - updateTaskNotificationStoreWithSettings(notifications, maxNotifications, "{}"); + String settings = String.format("{maxNotifications: %s}", maxNotifications); + updateTaskNotificationStoreWithSettings(notifications, settings); } @JavascriptInterface public void updateTaskNotificationStoreWithSettings( - String notifications, long maxNotifications, String settings) { + String notifications, String settings) { AppDataStore appDataStore = AppDataStore.getInstance(parent.getApplicationContext()); - appDataStore.saveTaskNotificationSettingsBlocking(settings, maxNotifications, notifications); + appDataStore.saveTaskNotificationSettingsBlocking(settings, notifications); } @android.webkit.JavascriptInterface diff --git a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java index 4af0f64b..243eeb14 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java +++ b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java @@ -31,8 +31,7 @@ public Result doWork() { AppDataStore appDataStore = AppDataStore.getInstance(context); AppNotificationManager appNotificationManager = new AppNotificationManager(context); try { - String notificationWindowSettings = appDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}"); + JSONObject notificationWindowSettings = AppNotificationManager.getSettings(appDataStore); if (isNotificationWindow(notificationWindowSettings)) { String notifications = appDataStore .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); @@ -45,13 +44,12 @@ public Result doWork() { } } - boolean isNotificationWindow(String windowObject) throws JSONException { - JSONObject data = Utils.parseJSONObject(windowObject); - if (!data.has("start") || !data.has("end")) { + boolean isNotificationWindow(JSONObject settings) throws JSONException { + if (!settings.has("start") || !settings.has("end")) { return true; } - LocalTime start = Utils.formatTime(data.getString("start")); - LocalTime end = Utils.formatTime(data.getString("end")); + LocalTime start = Utils.formatTime(settings.getString("start")); + LocalTime end = Utils.formatTime(settings.getString("end")); if (start == null || end == null) { return false; } diff --git a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java index 1b0b5847..d9d8c9f5 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java +++ b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java @@ -65,6 +65,11 @@ public void saveLongBlocking(String key, Long value) { Preferences ignored = updateResult.blockingGet(); // NOSONAR } + public void saveStringBlocking(String key, String value) { + Single updateResult = save(PreferencesKeys.stringKey(key), value); + Preferences ignored = updateResult.blockingGet(); // NOSONAR + } + private T getBlocking(Key key, @Nullable T defaultValue) { return dataStore .data() @@ -86,18 +91,13 @@ public long getLongBlocking(String key, @Nullable Long defaultValue) { /** * Persists the task-notifications, settings and max-count in a single atomic transaction. */ - public void saveTaskNotificationSettingsBlocking( - String settings, - long max, - String notifications) { + public void saveTaskNotificationSettingsBlocking(String settings, String notifications) { try { Preferences ignored = dataStore .updateDataAsync(preferences -> { MutablePreferences mutablePreferences = preferences.toMutablePreferences(); mutablePreferences.set(PreferencesKeys .stringKey(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY), settings); - mutablePreferences.set(PreferencesKeys - .longKey(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY), max); mutablePreferences.set(PreferencesKeys .stringKey(AppNotificationManager.TASK_NOTIFICATIONS_KEY), notifications); return Single.just(mutablePreferences); diff --git a/src/test/java/org/medicmobile/webapp/mobile/AppNotificationManagerTest.java b/src/test/java/org/medicmobile/webapp/mobile/AppNotificationManagerTest.java index 77321ffc..162f0bad 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/AppNotificationManagerTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/AppNotificationManagerTest.java @@ -38,6 +38,7 @@ public void setup() { appNotificationManager = spy(new AppNotificationManager(context)); startOfDay = appNotificationManager.getStartOfDay(); appDataStore = AppDataStore.getInstance(context); + appDataStore.saveStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{\"maxNotifications\": 8}"); useBlockingDataStoreGet(); } @@ -46,7 +47,7 @@ public void resetDataStore() { appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); appDataStore.saveLong(AppNotificationManager.TASK_NOTIFICATION_DAY_KEY, 0L); appDataStore.saveLong(AppNotificationManager.LATEST_NOTIFICATION_TIMESTAMP_KEY, 0L); - appDataStore.saveLong(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, 8L); + appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{\"maxNotifications\": 8}"); } @Test @@ -65,7 +66,7 @@ public void showsAndDismissesAllNotifications() throws JSONException { @Test public void showsOnlyMaxAllowedNotifications() throws JSONException { String jsData = "[" + getJSTaskNotificationString(startOfDay, startOfDay, startOfDay) + "]"; - appDataStore.saveLongBlocking(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, 1L); + appDataStore.saveStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{\"maxNotifications\": 1}"); appNotificationManager.showNotificationsFromJsArray(jsData); assertEquals(1, shadowNotificationManager.getAllNotifications().size()); } @@ -87,7 +88,7 @@ public void respectsNotificationsOrderAndStoreLatestReadyAtTimestamp() throws JS } ] """.formatted(jsData, latestReadyAtTimestamp, startOfDay, startOfDay); - appDataStore.saveLongBlocking(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, 1L); + appDataStore.saveStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{\"maxNotifications\": 1}"); appNotificationManager.showNotificationsFromJsArray(newNotificationData); assertEquals(1, shadowNotificationManager.getAllNotifications().size()); assertEquals(latestReadyAtTimestamp, appDataStore.getLongBlocking(AppNotificationManager.LATEST_NOTIFICATION_TIMESTAMP_KEY, 0L)); diff --git a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java index 0987357c..9a10d6d3 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java @@ -181,7 +181,7 @@ public void isNotificationWindow_returnsFalse_whenBadTimeFields() throws Excepti NotificationWorker worker = createWorker(); - boolean result = worker.isNotificationWindow(windowSettings); + boolean result = worker.isNotificationWindow(Utils.parseJSONObject(windowSettings)); assertFalse(result); } } @@ -198,7 +198,7 @@ public void isNotificationWindow_returnsTrue_whenWithinWindow() throws Exception NotificationWorker worker = createWorker(); - boolean result = worker.isNotificationWindow(windowSettings); + boolean result = worker.isNotificationWindow(Utils.parseJSONObject(windowSettings)); assertTrue(result); } } @@ -215,7 +215,7 @@ public void isNotificationWindow_returnsFalse_whenBeforeWindow() throws Exceptio NotificationWorker worker = createWorker(); - boolean result = worker.isNotificationWindow(windowSettings); + boolean result = worker.isNotificationWindow(Utils.parseJSONObject(windowSettings)); // Assert assertFalse(result); @@ -234,7 +234,7 @@ public void isNotificationWindow_returnsFalse_whenAfterWindow() throws Exception NotificationWorker worker = createWorker(); - boolean result = worker.isNotificationWindow(windowSettings); + boolean result = worker.isNotificationWindow(Utils.parseJSONObject(windowSettings)); // Assert assertFalse(result); From babe53e504c10f1768961ae743b871378ac41c37 Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Wed, 1 Jul 2026 10:24:07 +0300 Subject: [PATCH 14/15] clean up sonar warnings --- src/main/java/org/medicmobile/webapp/mobile/Utils.java | 2 +- .../java/org/medicmobile/webapp/mobile/util/AppDataStore.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/medicmobile/webapp/mobile/Utils.java b/src/main/java/org/medicmobile/webapp/mobile/Utils.java index acbbe136..22f2513a 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/Utils.java +++ b/src/main/java/org/medicmobile/webapp/mobile/Utils.java @@ -60,7 +60,7 @@ static boolean isUrlRelated(String appUrl, String uriToTest) { */ static boolean isValidNavigationUrl(String appUrl, String navUrl) { boolean isValid = isUrlRelated(appUrl, navUrl); - if (isValid && !navUrl.matches(".*/(login|_rewrite).*")) { + if (isValid && !navUrl.matches(".*/(login|_rewrite).*")) { // NOSONAR return true; } return false; diff --git a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java index d9d8c9f5..fca126c4 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java +++ b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java @@ -93,7 +93,7 @@ public long getLongBlocking(String key, @Nullable Long defaultValue) { */ public void saveTaskNotificationSettingsBlocking(String settings, String notifications) { try { - Preferences ignored = dataStore + Preferences ignored = dataStore // NOSONAR .updateDataAsync(preferences -> { MutablePreferences mutablePreferences = preferences.toMutablePreferences(); mutablePreferences.set(PreferencesKeys @@ -103,7 +103,7 @@ public void saveTaskNotificationSettingsBlocking(String settings, String notific return Single.just(mutablePreferences); }) .timeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .blockingGet(); // NOSONAR + .blockingGet(); } catch (Exception e) { log(e, "AppDataStore :: saving task notification settings failed/timed out"); } From 9d765efb668e25352a44ad9269e0b910f61be33b Mon Sep 17 00:00:00 2001 From: jonathanbataire Date: Wed, 1 Jul 2026 10:27:11 +0300 Subject: [PATCH 15/15] test sonar error clean up --- src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java b/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java index a6801961..4f598f02 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java @@ -272,13 +272,13 @@ public void testPackageManagerException() throws PackageManager.NameNotFoundExce } @Test - public void formatTime_returnsNullForBadTime() throws Exception { + public void formatTime_returnsNullForBadTime() { LocalTime result = Utils.formatTime("14:70"); assertNull(result); } @Test - public void formatTime_parsesValidTimeFormats() throws Exception { + public void formatTime_parsesValidTimeFormats() { String[] testTimes = {"00:00", "12:00", "23:59"}; LocalTime[] expectedTimes = { LocalTime.of(0, 0),