Skip to content

Conversation

@jonathanbataire
Copy link
Contributor

@jonathanbataire jonathanbataire commented May 26, 2025

Description

This PR adds tasks notification capabilities

  • Uses a worker manager scheduled every 15 minutes to check for notifications from the core here

related to medic/cht-core#9255

Closes #93

@jonathanbataire jonathanbataire marked this pull request as ready for review June 4, 2025 07:02
@jonathanbataire jonathanbataire changed the title feat: Adds notifications feat: Adds tasks notification Jun 4, 2025
Copy link
Contributor

@jkuester jkuester left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this is really coming together! Thank you for all your efforts here!

private boolean hasCheckedForNotificationApi = false;
private boolean hasTaskNotificationsApi = false;

private static volatile AppNotificationManager instance;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this to be volatile/synchronized now? It should be okay if the background worker creates its own instance of this class right?

Copy link
Contributor Author

@jonathanbataire jonathanbataire Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its used in MedicAndroidJavascript class as well so thats at least 3 instances
if we want to maintain a single instance of this helper which i think is great implementation since we only have a single channel then:

  • create the instance in embeddedbrowser's oncreate and keep passing it as param where possible.
  • or a singleton

also with a singleton i count a max of 2 instances on for EmbeddedBrowser and one for BG worker

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I might be wrong here, but I thought all the Android notification stuff was synced at the system level based on the CHANNEL_ID value. So, we should never end up with some kind of "real" duplicate notification channels at the system level.

In general I think it is good to avoid duplicating instantiated objects when you can re-use them just for the slight performance benefits. But, the extra performance overhead caused by volatile/synchronized means that I don't think it is worth the extra complexity except when:

  • It is really expensive to instantiate an object.
  • The state of the object has to be shared between threads.

I don't think either of those apply here, right? I still think it is fine to stick with the singleton pattern for the AppNotificationManager, but I am not sure we gain anything by re-using the instance across threads. 🤷

Copy link
Contributor Author

@jonathanbataire jonathanbataire Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont have strong opinion about this i dont think re instantiating the object is that expensive (ive been doing this myself) will change this implementation and remove the singleton

void initAppNotification(WebView webView, Activity activity) {
foregroundNotificationHandler = new NotificationForegroundHandler(webView, appUrl);
manager.cancelAll();
if (hasTaskNotificationsApi) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid keeping this value as state. It is just adding an extra thing that we have to keep track of. I think it is fine to ask the user for the notification permission regardless of if their CHT instance supports notifications or not. (Presumably all CHT instances will eventually be upgraded to support this feature anyway.) By not caching this value, we don't have to worry about somehow busting the cache when their CHT instance gets upgraded...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk didnt want to start these processes at all especially GB worker if CHT has not API

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also this is used to stop notification workers when user is logged out

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk didnt want to start these processes at all especially GB worker if CHT has not API

Why? If the concern is around performance, then it feels like we need a different mechanism for disabling notifications anyway because all supported versions of the CHT will eventually support the notifications API (but the performance implications on the client device will still exist regardless).

also this is used to stop notification workers when user is logged out

It still feels more safe/reliable/predictable to just always try to stop the notifications (without trying to keep track of the current status).

Having state like this in the AppNotificationManager forces a lot of down-stream complexity that I really do not see the benefits of. If a particular user does not want notifications, they can just deny the permission and we will not run anything. Do we need a way for a whole deployment to disable notifications?

}
}

void initAppNotification(WebView webView, Activity activity) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but is there a reason to have this function be distinct from startForegroundNotificationHandler? I am thinking of a linear init flow:

  • Start with a call to startForegroundNotificationHandler
    • This calls requestNotificationPermission
      • Checks hasNotificationPermission - if true. then requestNotificationPermission returns true.
      • If false, request the permission and return false.
  • If requestNotificationPermission returns false, the startForegroundNotificationHandler returns without doing anything because we have no notification permission (yet).
  • The EmbeddedBrowserActivity.onRequestPermissionsResult then calls startForegroundNotificationHandler again when the permission is granted.
  • In startForegroundNotificationHandler when requestNotificationPermission returns true, then we proceed to start the handler.

public Result doWork() {
Log.d(DEBUG_TAG, "background worker running......");
latch = new CountDownLatch(1);
Handler handler = new Handler(Looper.getMainLooper());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not expect us to need the Handler here in the NotificationWorker. I thought that the Android system would handle periodically executing the worker (without us needing to worry about manually looping inside the worker...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the handlers job here is to get the main thread and post work to it
which is the only place a webview would work without this nothing webview related will work

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, what does "the main thread" mean in the context of a background worker? Can we not instantiate a new WebView directly in the NotificationWorker class (as a part of the background worker thread)?

If not, how confident are we that all of this will work reliably (especially across different Android systems/versions)? (And sorry in advance if this is some kind of common pattern for doing thing that I am just not familiar with... 😅 I just know that reliable background notifications are always problematic in Android when you are outside the Firebase paradigm and I want to be sure we are not setting ourselves up for failure here...

Copy link
Contributor Author

@jonathanbataire jonathanbataire Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webview cant run on the background thread
when the system executes the background job 2 threads exist bg thread and the main app process thread aka main thread
bg thread posts work on the main app process thread via the handler that executes it
this works well from all my tests
this process is handled by the system (not a custom implementation) so it should work across android versions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have links to any documentation that would suggest launching a handler on the main looper from a background worker is a supported operation? I understand that you have testing this and it worked for you, but I am very concerned about the reliability of this approach (especially across different Android systems/versions).

Android is already super tricky around reliable background operations (as seen by the on-going saga of most other FLOSS apps that try to support notifications without Firebase...). Adding unsupported UI thread execution is not going to help this be more reliable... 😬

I have done a bunch more research today around this and keep coming back to this:

  • A WebView can only be created in the "UI Thread" (as you have noted).
  • Running a handler on the main looper (as we are doing here) does effectively execute the handler on the main UI thread. As long as you have an application context (as we do here in the Background Worker).
  • However, views (including WebViews) should only be created with an Activity in context. Any creation of a view without an Activity is technically unsupported.
  • In almost all cases, activities cannot be launched from the background (without user input).
  • Even foreground services (e.g. with a persistent notification) cannot reliably start activities (without user input).
  • There is no reliable way to keep an activity running indefinitely.

We need a WebView to run the webapp and check for notifications. The only supported way to get a WebView is with an Activity in context, but there is nor reliable way to get an activity in context when the app is not open. My conclusion is that there does not seem to be any supported way to achieve notifications with our current architecture. Please, please, correct me here if I am missing something!

All this being said, it seems like a hard thing to come all this way and have notifications being successfully pushed by your current approach, but then we give up because it is not technically supported (especially when we don't really have any other viable options). @jonathanbataire I understand you have a partner that is interested in using these notifications in production. Would it be possible to build them an APK to use that is based on this branch (without merging to master)? We could then collect some telemetry data about how things are working in the real world and let that inform the viability of merging this PR in the future and providing the feature to the broader community.


On a somewhat related note, since we run this notification check in the main UI thread, does this mean that every time we check for notifications, the device will become unresponsive? When the app is in context, all we are doing is making a quick JS call, but here in the background worker we have to spin up the whole WebView/webapp before making the JS call. Just worried that the UX here may not be good if we lock the UI thread even for a few seconds...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what it looks like in the device as you can see its downloaded/updated from play store not an OS update by the manufacturer
Screenshot_20250925_072103_Settings

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Device compatibility issues in Android are a big enough problem for open source apps that folks are writing research papers about it.

  • device doesnt have google services like huawei devices implement there own thing

We dont and shouldnt cater for any of the above

I understand where you are coming from with custom ROMS (I use GrapheneOS at my own risk 😅 and it def does not bundle the Android System Webview from the Play Store...). However, I think we definitely need to support OEM ROMs that do not bundle Google Play Services. Without global devices stats for the CHT, we cannot be sure exactly how common these are in the field, but I do know that our experience working with various programs indicates we cannot assume everyone is going to have a working copy of Google Play Services...

Hopefully this is all a nothing-burger and our usage here works fine on all devices across-the-board. There is just a lot of history around the Android System Webview that we have struggled with ever since ditching Crosswalk. And I am cautions when it comes to making any assumptions in this area....

again we can test this across different devices first if it helps
if indeed we want to pilot this first this is not the reason to base this decision on imo

I think this is a fine place to land the conversation. We don't need to agree on on the risk here. Lets move forwards with the pilot and we can collect data about how this is getting used. Then we will be well positioned to promote this feature when we release it for the rest of the community. (Or, worst case scenario, we realize this is not going to work as we hoped....)

Copy link
Contributor Author

@jonathanbataire jonathanbataire Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see

However, I think we definitely need to support OEM ROMs that do not bundle Google Play Services.

do you have an example of this? there is no OEM that ships without play services as far as i know (except some huawei phones that dont sell outside china)
making this argument would mean CHT android as a whole wont work because the main webview uses this default android webview and is written assuming that
alot of android features assume device has play services including GPS in this app
The app is not built for this scenario (i dont think we should) even besides this feature

I use GrapheneOS at my own risk 😅 and it def does not bundle the Android System Webview from the Play Store...

there is no device that ships with these ROMs im pretty sure you installed this yourself and know the risks its comes with even beyond just webviews

but I do know that our experience working with various programs indicates we cannot assume everyone is going to have a working copy of Google Play Services...

these are system level components that require and are updated all the time thru play store they are the same for all devices

There is just a lot of history around the Android System Webview that we have struggled with ever since ditching

yes CHT android supports webview 90+ and I see it fail all the time in the field (gets stuck on loading page). This is because someone is running an old webview. the solution is to always update webview to the latest version and everything works fine.

i still dont see a genuine reason to veto this to be frank with you

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i still dont see a genuine reason to veto this to be frank with you

I don't believe that a veto is what you're seeing here @jonathanbataire , it's just caution before releasing some code we are not completely confident with, and, by consequence, making ourselves responsible for maintaining it for future releases.
I agree with @jkuester's caution. We have had smaller issues.

WebView should always be instantiated with an Activity Context. If instantiated with an Application Context, WebView will be unable to provide several features, such as JavaScript dialogs and autofill.

I have seen this note i think its just a warning it shouldnt be taken that app context wont work it just highlights the limitations
its just a limitation not that it wont/doesnt work
none of these limitation affect this implementation

It's true that some "notes" have less meaning than others, but I don't think we should disregard this so easily, just because it works on a few devices. At the least, we must know what is the list of features that are not available in this "headless" webview.

I agree with @jkuester 's suggestion to launch this as a pilot, as a "feature release" for CHT + CHT Android and see how it behaves over time. This is our general approach for things that are more on the experimental side.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay 👍🏿

@Override
public Result doWork() {
Log.d(DEBUG_TAG, "background worker running......");
latch = new CountDownLatch(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we need this latch for? I am having trouble understanding what we are protecting against here...

Copy link
Contributor Author

@jonathanbataire jonathanbataire Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i need a way to timeout this execution so it doesn't run forever whether inside JS context or Java context
the countdownlatch is used for this purpose
it returns a value to stop work manager as soon as js executes and notification sent or execution is taking more than 10 secs
in otherwords caps execution to less than 10 secs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so it doesn't run forever whether inside JS context or Java context

But we don't have any operations here that we anticipate could possibly just run indefinitely, right? We are executing an async operation that should either succeed or fail, but it is not designed to re-try indefinitely or anything like that.... If somehow the background thread does get stuck because of an unanticipated issue, Android, itself, could end up killing it or at the very least it will get closed the next time the user opens the app and we cancel all the background workers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

android's timeout is 10 mins
so in case anything happens eg js function cant be accessed, this worker will run for 10 mins

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in case anything happens eg js function cant be accessed

I think this is still where I am lost. 🤔 Why would the JS execution just hang indefinitely instead of just succeeding or failing at runtime? Is there something unique to our situation here that I am not considering, or is this a problem that could be faced in all the other JS calls in cht-android?

Copy link
Contributor

@jkuester jkuester Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I see what you are saying about the callback not working (classic Android to be half-way to something useful, but not actually cover obvious cases like async JS...). 🤦

Regardless, the callback is not important. Can just switch back to an interface. This seems to work as expected for me:

	private static final String NOTIFICATION_JS = """
        (async () => {
            const tasks = await window?.CHTCore?.AndroidApi?.v1?.taskNotifications();
            NotificationWorker.onGetNotificationResult(JSON.stringify(tasks ?? []));
        })();
        """;

	private final WebViewClient onFinishedClient = new WebViewClient() {
		@Override
		public void onPageFinished(WebView view, String url) {
			view.evaluateJavascript(NOTIFICATION_JS, null);
		}
	};

	@NonNull
	@Override
	@SuppressLint("SetJavaScriptEnabled")
	public ListenableFuture<Result> startWork() {
		Handler handler = new Handler(Looper.getMainLooper());
		return CallbackToFutureAdapter.getFuture(completer -> {
			handler.post(() -> {
				WebView webView = new WebView(getApplicationContext());
				webView.getSettings().setJavaScriptEnabled(true);
				Object notificationInterface = new Object() {
					@JavascriptInterface
					public void onGetNotificationResult(String data) {
						try {
							AppNotificationManager appNotificationManager = new AppNotificationManager(getApplicationContext());
							appNotificationManager.showNotificationsFromJsArray(data);
							Log.d(DEBUG_TAG, "notification worker ran successfully!");
							completer.set(Result.success());
						} catch (Exception e) {
							log(e, "error checking for notifications");
							completer.set(Result.failure());
						} finally {
							destroyWebView(webView);
						}
					}
				};
				webView.addJavascriptInterface(notificationInterface, "NotificationWorker");
				enableStorage(webView);
				webView.setWebViewClient(onFinishedClient);
				webView.loadUrl(appUrl);
			});
			return "Async notification check";
		});
	}

Copy link
Contributor Author

@jonathanbataire jonathanbataire Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this throws error in the finally block when cleaning up webview
Are you able to get the same?
java.lang.Throwable: A WebView method was called on thread 'JavaBridge'. All WebView methods must be called on the same thread.

also doesnt seem to work for me (showing notifications)
it seems js checks (optional chaining) fail and it returns []
it not consistent sometimes it works sometimes not

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this throws error in the finally block when cleaning up webview
Are you able to get the same?

Yeah, I was able to repro this. Just needed to tweak the destroyWebView call to run in the main thread like you had it before:

						} finally {
							handler.post(() -> destroyWebView(webView));
						}

also doesnt seem to work for me (showing notifications)
it seems js checks (optional chaining) fail and it returns []
it not consistent sometimes it works sometimes not

So, I did some more testing here. Actually, once I fixed the destroy error it stopped showing any notifications for me at all (which seemed weird). It turns out, what is happening is the onPageFinished gets triggered multiple times when we call webView.loadUrl(appUrl);. This is because loading the webapp ends up causing the page to get re-directed multiple times. The first call to onPageFinished is just for the root page, /, and the app is not loaded yet. So, the NOTIFICATION_JS would return empty. Then, when the app is loaded, it redirects to /home which triggers another onPageFinished call. Now the app is loaded an the NOTIFICATION_JS call can return actual notifications. Then the webapp redirects me for a final time to /messages and onPageFinished is called again (and returns the same notification data!??!)... I am not sure how your original code was handling these multiple calls to onPageFinished??? (Maybe the latch was sorting it out or something else was going on....)

I got everything to play nice with an updated version of the onFinishedClient:

	private final WebViewClient onFinishedClient = new WebViewClient() {
		boolean triggered = false;
		@Override
		public void onPageFinished(WebView view, String url) {
			if (triggered || (appUrl + "/").equals(url)) {
				return;
			}
			triggered = true;
			view.evaluateJavascript(NOTIFICATION_JS, null);
		}
	};

It seems to work as expected (prevents running the NOTIFICATION_JS on the root URL (when the app is not loaded) and re-running it for the /messages redirect), but if feels a bit brittle. Maybe you have some ideas on how to do this better? So far I have not come up with a good way to "run this when the webapp finishes bootstrapping"...

Copy link
Contributor Author

@jonathanbataire jonathanbataire Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice sleuthing!
yeah it does a lot of looping not sure why
the latch would just let it do its thing and only stop waiting once notifications are sent thats why it works

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated....

}

@JavascriptInterface
public void onGetNotificationResult(String data, String appUrl) throws JSONException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably depends on the responses to my questions above, but I wonder if we can avoid duplicating this function here and in MedicAndroidJavascript. I am thinking maybe we could just have one version of this function on the AppNotificationManager and then the NotificationForegroundHandler could add it as a Javascript interface similar to how we do it in this file...

Comment on lines 120 to 127
wv.stopLoading();
wv.getSettings().setJavaScriptEnabled(false);
wv.loadUrl("about:blank");
wv.clearHistory();
wv.clearCache(true);
wv.removeAllViews();
wv.destroy();
wv = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this feels a bit redundant since we are calling destroy on the WebView.

Since we are not attaching the webview to anything or adding any child views, I guess maybe all we really need here is to call stopLoading and destroy. Right?

Copy link
Contributor Author

@jonathanbataire jonathanbataire Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see
some of these are supposed to make it faster for android to take down the webview but let me look at it some more

@jkuester
Copy link
Contributor

jkuester commented Oct 3, 2025

Sorry that I have not been able to review the updates here yet! I ran out of time this week, but I have not forgotten it and it is in the queue for next week. 👍

Copy link
Contributor

@jkuester jkuester left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, we are getting close here! 💪 Couple more mostly minor things....

private final String appUrl = settings.getAppUrl();
private WebView webView;
private final Handler handler = new Handler(Looper.getMainLooper());
String notificationsJS = "(async () => { " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, can be private.

Suggested change
String notificationsJS = "(async () => { " +
private String notificationsJS = "(async () => { " +

@Override
public void onStopped() {
super.onStopped();
destroyWebView(webView);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 👍

@SuppressLint("SetJavaScriptEnabled")
@NonNull
@Override
public ListenableFuture<Result> startWork() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can resolve the Sonar cognitive complexity issue with this method by just keeping the web view client as a top-level final variable (instead of creating it on-demand in this method):

	private final WebViewClient webViewClient = new WebViewClient() {
		boolean triggered = false;
		@Override
		public void onPageFinished(WebView view, String url) {
			if (triggered || (appUrl + "/").equals(url)) {
				return;
			}
			triggered = true;
			view.evaluateJavascript(notificationsJS, null);
		}
	};

private void enableStorage(WebView wv) {
WebSettings webSettings = wv.getSettings();
webSettings.setDomStorageEnabled(true);
webSettings.setDatabaseEnabled(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
webSettings.setDatabaseEnabled(true);

I think we can remove this line (and fix the deprecation issue), both here and in the EmbeddedBrowserActivity. According to the docs linked from the deprecation notice, this feature is related to WebSQL. I could not find any evidence that tech was ever used in the CHT (FireFox never supported it). So, I do not think we ever actually needed this call. It seems to have been added when we ripped xwalk out, but I don't think it was necessary....

appNotificationManager = new AppNotificationManager(this, foregroundNotificationHandler);
appNotificationManager.cancelAllNotifications();
appNotificationManager.requestNotificationPermission(this);
appNotificationManager.startForegroundNotificationHandler();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this here since we are already calling this later in onResume, right?

Suggested change
appNotificationManager.startForegroundNotificationHandler();

manager.cancelAll();
}

void showMultipleTaskNotifications(JSONArray dataArray) throws JSONException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be private:

Suggested change
void showMultipleTaskNotifications(JSONArray dataArray) throws JSONException {
private void showMultipleTaskNotifications(JSONArray dataArray) throws JSONException {

}
}

void showNotification(Intent intent, int id, String title, String contentText) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be private.

Suggested change
void showNotification(Intent intent, int id, String title, String contentText) {
void showNotification(Intent intent, int id, String title, String contentText) {

String contentText = notification.getString("contentText");
String title = notification.getString("title");
long readyAt = notification.getLong("readyAt");
int notificationId = (int) (readyAt % Integer.MAX_VALUE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Is there a reason we should use the readyAt value as the unique id instead of just getting the hash for the actual task id?

        int notificationId = notification
            .getString("_id")
            .hashCode();

postTask();
return;
}
webView.setWebViewClient(new WebViewClient(){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we want to call setWebViewClient here since I think that will replace the UrlHandler client set in EmbeddedBrowserActivity.

The UrlHandler already has a onPageFinished method that broadcasts a onPageFinished intent. I wonder if we could just trigger posting the task via a BroadcastReceiver like this?


private final Runnable task;
private final WebView webView;
private boolean isPageLoaded = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor but I think isTaskPosted would be a more descriptive name of what this value is actually indicating.

@dianabarsan
Copy link
Member

Sorry for barging in, but I had an idea of how to change this so it's a safer Android bet.
The issue is that we need to grab tasks when the App is not running. My idea is that every time Webapp recalculates tasks, it would sent them to the Android app, along with their due date and status, and the Android app would store them in some alternative storage. The data nugget would only contain what is necessary to display the notification.
Then, the Android app, on whatever schedule we desire, would fire notifications for the tasks that become due.

there are only three situations that would get tasks to change:

  1. user actions (new reports for example) that require the app to be used
  2. settings changes - require sync and complete recalculation
  3. other user's actions - also require sync and recalculation

Both 2 and 3 require an internet connection and a successful sync. I don't know if the background process that this PR is launching now can achieve the sync before the tasks get sent - I believe there is no Webapp support for that - and triggering sync every x minutes while the user is not actually using the app would add a lot of load on the server, so it's unlikely we desire this.

With this said, I think we would cover 95% of useful notifications using preloaded tasks in Android.

@jonathanbataire and @jkuester please let me know what you think.
This would make the Android code significantly simpler, less risky and it would likely not need a pilot and feature release to test it, we could just release it as-is!

@jonathanbataire
Copy link
Contributor Author

jonathanbataire commented Oct 8, 2025

Sorry for barging in, but I had an idea of how to change this so it's a safer Android bet.
The issue is that we need to grab tasks when the App is not running. My idea is that every time Webapp recalculates tasks, it would sent them to the Android app, along with their due date and status, and the Android app would store them in some alternative storage. The data nugget would only contain what is necessary to display the notification.
Then, the Android app, on whatever schedule we desire, would fire notifications for the tasks that become due.

there are only three situations that would get tasks to change:

  1. user actions (new reports for example) that require the app to be used
  2. settings changes - require sync and complete recalculation
  3. other user's actions - also require sync and recalculation

Both 2 and 3 require an internet connection and a successful sync. I don't know if the background process that this PR is launching now can achieve the sync before the tasks get sent - I believe there is no Webapp support for that - and triggering sync every x minutes while the user is not actually using the app would add a lot of load on the server, so it's unlikely we desire this.

With this said, I think we would cover 95% of useful notifications using preloaded tasks in Android.

@jonathanbataire and @jkuester please let me know what you think.
This would make the Android code significantly simpler, less risky and it would likely not need a pilot and feature release to test it, we could just release it as-is!

so basically:

  • create an android notification store eg shared prefs
  • create logic to update the store and expose this to angular via medic_android bridge
  • on CHT side whenever tasks are recalculated update notifications store in android (probably need to fix the size of this blob for simplicity and avoid storing too much in android without a DB)
  • run a back ground worker in android to read and fire notifcations for due/overdue tasks. (should this also update the notification store if data is stale?)

@dianabarsan all tasks are recalculated on new report(report edit?) and settings change right? just so I get you right

so if we update notification store every time recalculation occurs without minding how it happens this should keep the store pretty much up to date

@dianabarsan
Copy link
Member

@jonathanbataire Exactly!

run a background worker in android to read and fire notifcations for due/overdue tasks. (should this also update the notification store if data is stale?)

So, I don't know what it means that the data is stale. I suppose that you're talking about cases where the CHT has not opened the app in 10 days - for example. In this case, I don't think we know that the data is stale and we should notify anyway. We only know the data is stale if we open the app, sync and recalculate. So this is Schrodinger's data, both stale and not stale :D
In most cases, the data is not stale and we're safe to notify about it I believe.

@jonathanbataire
Copy link
Contributor Author

We only know the data is stale if we open the app, sync and recalculate. So this is Schrodinger's data, both stale and not stale :D

hahaha
i think i see it will open new PRs for this

@jkuester
Copy link
Contributor

jkuester commented Oct 8, 2025

create an android notification store eg shared prefs

FWIW, I think SharedPrefs are pretty good for basic key-value storage, but they leave something to be desired when it comes to more complex scenarios. In a different app, I have had good experiences with the Android Datastore library (though that was with Kotlin, so the Java interfaces may not be so nice...). Given that our data is coming from a types-optional JS context anyway, we can probably go with the simple Preferences DataStore (instead of adding the extra overhead of Protobuf). 🤷

@jonathanbataire
Copy link
Contributor Author

create an android notification store eg shared prefs

FWIW, I think SharedPrefs are pretty good for basic key-value storage, but they leave something to be desired when it comes to more complex scenarios. In a different app, I have had good experiences with the Android Datastore library (though that was with Kotlin, so the Java interfaces may not be so nice...). Given that our data is coming from a types-optional JS context anyway, we can probably go with the simple Preferences DataStore (instead of adding the extra overhead of Protobuf). 🤷

I'm using datastore

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create Android notifications from Tasks

6 participants