Add HTA-side implementation of sync-interrupted scenario (HT-508). Also replace deprecated AsyncTask.#13
Conversation
…stead of hardcoding "success". Replace deprecated AsyncTask. Added handling for the HttpRequest from HT which now can have either "sync_success" or "sync_interrupted". Formerly the user would see a success message in either case. Now, however, when a sync is cancelled the user will see a message indicating that sync did not complete. Replaced AsyncTask (which is deprecated) with Executors and Handlers. This still contains debug output from the investigation activities. A future commit (very soon, hopefully) will remove this in PR preparation.
…Also replace deprecated AsyncTask code. Formerly HT sent HTA "sync_success" regardless of whether 'mergeCompleted' was True. Now the False case leads to HTA receiving "sync_interrupted" and HTA informs the user that sync did not complete successfully. Also, deprecated class 'AsyncTask' is replaced with 'Executors' and 'Handlers'.
tombogle
left a comment
There was a problem hiding this comment.
@tombogle reviewed 3 of 3 files at r1, all commit messages.
Reviewable status: all files reviewed, 4 unresolved discussions (waiting on @wmergenthal)
app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java line 43 at r1 (raw file):
// // NOTE: like several things in HearThisAndroid, HttpRequest is deprecated. It will be // replaced with something more appropriate, hopefully soon.
Options:
-
Use
HttpURLConnection(built-in JDK/Android)
→ Standard replacement for most simple request/response work.-
Pros: No external dependency.
-
Cons: Verbose API; no built-in high-level features.
-
-
Use
OkHttp(square.github.io/okhttp)
→ De facto standard for Android networking.-
Pros: Clean API, better performance, supports HTTP/2, widely adopted.
-
Cons: Adds dependency, but well worth it in most apps.
-
app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java line 49 at r1 (raw file):
String s2 = s1.substring(s1.indexOf(' ') + 1, s1.lastIndexOf(' ')); String status = s2.substring(s2.indexOf('=') + 1);
This logic could be done in a less cryptic way:
String s1 = request.getRequestLine().getUri(); // avoid toString() if possible
URI uri = new URI(s1);
String query = uri.getQuery(); // e.g., "status=123&foo=bar"
String status = null;
for (String param : query.split("&")) {
String[] pair = param.split("=");
if (pair.length == 2 && pair[0].equals("status")) {
status = pair[1];
break;
}
}
Alternatively, it could be done very simply with a regular expression. But using URI is probably safer and makes the intent clear.
app/src/main/java/org/sil/hearthis/SyncActivity.java line 166 at r1 (raw file):
socket.send(packet); } catch (UnknownHostException e) { e.printStackTrace();
In the case of an exception, is there anything else we need to do to give feedback to the user and/or prevent the HTA app from getting stuck in a bad state?
app/src/main/java/org/sil/hearthis/SyncActivity.java line 280 at r1 (raw file):
setProgress(getString(R.string.sync_interrupted)); } else { // Should never happen. Not sure what to do here, if anything...
For the sake of future proofing, we should probably handle any unexpected message by letting the use know that they are probably using a version of HTA that is not fully compatible with HT desktop. Then probably we should treat it similar to the "interrupted" case, since we probably can't safely assume it was completed successfully.
Instead of raw string objects, use a URI object for status extraction from the HttpRequest from HT. Resulting logic is longer but clearer.
Treat similarly to when sync is interrupted. Show the user a non-success message and suggest that they verify that HT/HTA versions are close enough that HTA knows about all possible HT sync statuses.
|
|
||
| if (status == null) { | ||
| // Something went wrong. Make sure the user sees a non-success message. | ||
| status = "sync_interrupted"; |
There was a problem hiding this comment.
I wonder if we should display a different message in the case where we get an unexpected status message. I assume the processing should be the same (since we can't possibly know how else to interpret the unexpected message), but if this ever happens, we could give the user some indication that we got an unexpected message and suggest they check for an update. We could even include in the query (from HT) an indication of the minimum version of HTA known to handle the particular status message. Then if we ever get an unexpected status and it includes a version number as well, we could tell the user the minimum version of HTA that is compatible with the version of HT they are using. The HTA code would then look something like this:
String status = null;
String minHtaVersion = null;
try {
String s1 = request.getRequestLine().getUri();
URI uri = new URI(s1);
String query = uri.getQuery();
if (query != null) {
for (String param : query.split("&")) {
String[] pair = param.split("=", 2); // limit=2 in case value contains '='
if (pair.length == 2) {
if (pair[0].equals("message")) {
status = pair[1];
} else if (pair[0].equals("minHtaVersion")) {
minHtaVersion = pair[1];
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
if (status == null || isUnexpectedStatus(status)) {
if (minHtaVersion != null) {
showError("Unexpected sync status received. This version of HearThis for Android appears to be incompatible with your version of HearThis. Please update HearThis for Android to at least version " + minHtaVersion + ".");
} else {
showError("Unexpected sync status received. Please check for updates.");
}
status = "sync_interrupted"; // Or possibly a distinct status
}
There was a problem hiding this comment.
Same comment as for the flagged 'minHtaVersion' item above -- I want to leave it as is for now because the 'minHtaVersion' bit of mechanism is only partially implemented.
Nominal case works. Still need to test watchdog when HT fails to complete its side of sync. Debug output still present (which helps greatly understanding the mechanism).
No substantive changes. Just captures debug stmts producing the output captured in 20251001_02_HTA_sync_watchdog_all_ok.log, where watchdog is seen to work correctly for both success and fail scenarios.
Watchdog was being started too early. If the Android user was not quick to scan the QR code after getting into that mode, the watchdog could have timed out before the sync even started. Now we don't start it until Android has sent its UDP packet to PC to initiate a sync. Also, multiple watchdog shutdown() calls are now consolidated into a single one. Add comments to the new Watchdog class. Remove some dead code and improve some debug output stmts.
Without the Continue button the user can't get back to the home screen unless a subsequent sync retry is successful. To make the Continue button usable, add call to AcceptNotificationHandler like all the other spots have.
|
From an error handling/reporting standpoint, if we did get a query, it might be a good idea to display and/or log it in a way that it could be reported to us so we could try to figure out what happened. |
tombogle
left a comment
There was a problem hiding this comment.
@tombogle reviewed 2 files and all commit messages.
Reviewable status: 2 of 7 files reviewed, 6 unresolved discussions (waiting on @wmergenthal).
This is a coordinating change with the HT side. Last fall we had decided to change a few wordings, one of which is to change "sync_interrupted" to "sync_canceled". This comm does that as well as commenting out several debug output stmts. Debug output will be removed altogether before the next push, but this is an intermediate snapshot commit.
There are a handful of debug output things I want to capture in a way that enables easy and reliable transmission to our tech support. These are commented out along with a notation to "implement for tech support". (1) Timeout reduction, (2) minHtaVersion and (3) exception handling fixes are being postponed until after the Code-a-thon. Some of these might get done *during* the Code-a-thon, but even if they don't it will be preferable to develop these 3 things on the updated code base. The intent of this commit is to complete all needed changes requested for Pull Request sillsdev#13.
|
I hope this is helpful and doesn't just add more noise, but I now trust devin review more than a human to do reviews. https://app.devin.ai/review/sillsdev/HearThisAndroid/pull/13#issuecomment-3910931106
|
Watchdog.java - start timer as last action in constructor; make shutdown() synchronized like pet() is; remove unused import SyncActivity.java - fix thread violation by moving ipView call up into calling thread; add socket close; add executor shutdown; add null checks to watchdog calls AcceptNotificationHandler.java - add response body for minHtaVersion which is still only partially implemented
Devin pointed out that this gets done as part of listener.onNotification().
wmergenthal
left a comment
There was a problem hiding this comment.
I addressed all items except the ones involving 'minHtaVersion'. That should be left as is for now since it is a new bit of the mechanism that is only partially implemented, to be finished asap.
@wmergenthal made 11 comments.
Reviewable status: 0 of 7 files reviewed, 10 unresolved discussions (waiting on tombogle).
app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java line 43 at r1 (raw file):
Previously, tombogle (Tom Bogle) wrote…
Options:
Use
HttpURLConnection(built-in JDK/Android)
→ Standard replacement for most simple request/response work.
Pros: No external dependency.
Cons: Verbose API; no built-in high-level features.
Use
OkHttp(square.github.io/okhttp)
→ De facto standard for Android networking.
Pros: Clean API, better performance, supports HTTP/2, widely adopted.
Cons: Adds dependency, but well worth it in most apps.
I plan to leave this alone for now, and see if gets done during the Code-a-thon.
app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java line 49 at r1 (raw file):
Previously, tombogle (Tom Bogle) wrote…
This logic could be done in a less cryptic way:
String s1 = request.getRequestLine().getUri(); // avoid toString() if possible
URI uri = new URI(s1);
String query = uri.getQuery(); // e.g., "status=123&foo=bar"String status = null;
for (String param : query.split("&")) {
String[] pair = param.split("=");
if (pair.length == 2 && pair[0].equals("status")) {
status = pair[1];
break;
}
}Alternatively, it could be done very simply with a regular expression. But using URI is probably safer and makes the intent clear.
This has been implemented (with a slight modification).
app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java line 85 at r3 (raw file):
Previously, tombogle (Tom Bogle) wrote…
From an error handling/reporting standpoint, if we did get a query, it might be a good idea to display and/or log it in a way that it could be reported to us so we could try to figure out what happened.
Am postponing this until after the Code-a-thon. Or perhaps it will get done during that event, as a bonus, if things go well.
app/src/main/java/org/sil/hearthis/SyncActivity.java line 166 at r1 (raw file):
Previously, tombogle (Tom Bogle) wrote…
In the case of an exception, is there anything else we need to do to give feedback to the user and/or prevent the HTA app from getting stuck in a bad state?
This will be an item for study after the Code-a-Thon when dropped connection error scenarios are considered.
app/src/main/java/org/sil/hearthis/SyncActivity.java line 280 at r1 (raw file):
Previously, tombogle (Tom Bogle) wrote…
For the sake of future proofing, we should probably handle any unexpected message by letting the use know that they are probably using a version of HTA that is not fully compatible with HT desktop. Then probably we should treat it similar to the "interrupted" case, since we probably can't safely assume it was completed successfully.
Will be handled after the Code-a-Thon as part of the 'minHtaVersion' mechanism.
|
|
||
| if (status == null) { | ||
| // Something went wrong. Make sure the user sees a non-success message. | ||
| status = "sync_interrupted"; |
There was a problem hiding this comment.
Same comment as for the flagged 'minHtaVersion' item above -- I want to leave it as is for now because the 'minHtaVersion' bit of mechanism is only partially implemented.
| public synchronized void pet() { | ||
| if (watchdogTask != null && !watchdogTask.isDone()) { | ||
| watchdogTask.cancel(false); | ||
| } | ||
| watchdogTask = scheduler.schedule(onTimeout, timeout, unit); | ||
| } |
There was a problem hiding this comment.
🔴 Watchdog.pet() after shutdown() throws RejectedExecutionException, crashing the SyncServer thread
After watchdog.shutdown() is called (which calls scheduler.shutdownNow()), any subsequent call to watchdog.pet() will call scheduler.schedule(...) on a shut-down ScheduledExecutorService, throwing an uncaught RejectedExecutionException.
Root Cause and Impact
The race occurs between two threads:
- Watchdog scheduler thread: timeout fires →
onTimeoutruns → callslistener.onNotification("sync_error")→SyncActivity.onNotification()at line 301 callswatchdog.shutdown() - SyncServer thread (concurrently): handling a file request → calls
receivingFile()at line 346 →watchdog.pet()→scheduler.schedule()throwsRejectedExecutionException
Alternatively, a normal sync completion path has the same issue: AcceptNotificationHandler.handle() notifies listeners → onNotification → watchdog.shutdown(). Then a subsequent HTTP file request hits receivingFile()/sendingFile() and calls pet() on the shut-down watchdog.
The RejectedExecutionException is a RuntimeException not caught by the SyncServer's request loop (SyncServer.java:101-104 only catches IOException and HttpException), so it propagates up and terminates the SyncServer thread entirely, breaking all subsequent sync communication.
The Watchdog.pet() method at line 40 needs to guard against the scheduler being shut down, for example by checking scheduler.isShutdown() before scheduling.
| public synchronized void pet() { | |
| if (watchdogTask != null && !watchdogTask.isDone()) { | |
| watchdogTask.cancel(false); | |
| } | |
| watchdogTask = scheduler.schedule(onTimeout, timeout, unit); | |
| } | |
| public synchronized void pet() { | |
| if (scheduler.isShutdown()) { | |
| return; | |
| } | |
| if (watchdogTask != null && !watchdogTask.isDone()) { | |
| watchdogTask.cancel(false); | |
| } | |
| watchdogTask = scheduler.schedule(onTimeout, timeout, unit); | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
| } | ||
|
|
||
| if (status == null) { | ||
| // We got something but it wasn't "status". Make sure the user sees an error message. | ||
| status = "sync_error"; |
There was a problem hiding this comment.
🔴 Backward-incompatible: older HearThis desktop without query params triggers false sync_error
When an older HearThis desktop (pre-HT-508) sends POST /notify HTTP/1.1 without any query parameters, the new AcceptNotificationHandler.handle() code sets status = "sync_error" and shows the user an error message even though sync actually completed successfully.
Detailed Explanation
The old AcceptNotificationHandler.handle() unconditionally called listener.onNotification("") and responded with "success" for all notifications. The old SyncActivity.onNotification() unconditionally showed "Sync completed successfully!".
With the new code at AcceptNotificationHandler.java:57-83:
uri.getQuery()returnsnullfor/notify(no query string)- The for-loop is skipped entirely
statusremainsnull- Line 81-83 sets
status = "sync_error" - Listeners receive
"sync_error"→ user sees "Sync timed out or had some other error. Please try again."
This means any user running the updated HearThisAndroid app with an older HearThis desktop will always see a sync error, even on a perfectly successful sync. The fallback for missing query parameters should be "sync_success" (or at minimum not "sync_error") to maintain backward compatibility.
Prompt for agents
In AcceptNotificationHandler.java lines 79-83, when status is null (no query parameters found), the code defaults to "sync_error". This breaks backward compatibility with older HearThis desktop versions that don't include query parameters in their notification. Consider checking whether the minHtaVersion has been set (indicating a new HT desktop that supports the protocol) before defaulting to error. If minHtaVersion is null (old desktop), status should default to "sync_success" to preserve the previous behavior. If minHtaVersion is set but status is missing, then "sync_error" is appropriate. For example:
if (status == null) {
if (minHtaVersion != null) {
status = "sync_error";
} else {
status = "sync_success";
}
}
Was this helpful? React with 👍 or 👎 to provide feedback.

Formerly HT sent HTA "sync_success" regardless of whether 'mergeCompleted' was True. Now the False case leads to HTA receiving sync status "sync_interrupted". HTA parses the HttpRequest from HT to extract the status, then informs the user. One new string is added to HTA for the interrupt-or-cancelled case.
Also, deprecated class 'AsyncTask' is replaced with 'Executors' and 'Handlers'.
This change is