Skip to content

Add HTA-side implementation of sync-interrupted scenario (HT-508). Also replace deprecated AsyncTask.#13

Open
wmergenthal wants to merge 12 commits intosillsdev:masterfrom
wmergenthal:sync-cancel-added-asynctask-replaced
Open

Add HTA-side implementation of sync-interrupted scenario (HT-508). Also replace deprecated AsyncTask.#13
wmergenthal wants to merge 12 commits intosillsdev:masterfrom
wmergenthal:sync-cancel-added-asynctask-replaced

Conversation

@wmergenthal
Copy link
Collaborator

@wmergenthal wmergenthal commented Aug 28, 2025

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 Reviewable


Open with Devin

…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'.
Copy link
Contributor

@tombogle tombogle left a comment

Choose a reason for hiding this comment

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

@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.
@wmergenthal
Copy link
Collaborator Author

Re comment 1: thanks for suggesting some options. OkHttp certainly looks compelling.
Re comment 2: addressed in 3727d83.
Re comment 3: currently working on this one.
Re comment 4: addressed in 399e532.


if (status == null) {
// Something went wrong. Make sure the user sees a non-success message.
status = "sync_interrupted";
Copy link
Contributor

Choose a reason for hiding this comment

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

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
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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.
@tombogle
Copy link
Contributor

app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java line 85 at r3 (raw file):

        if (status == null) {
            // We got something but it wasn't "status". Make sure the user sees an error message.
            status = "sync_error";

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.

Copy link
Contributor

@tombogle tombogle left a comment

Choose a reason for hiding this comment

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

@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.
@andrew-polk
Copy link
Contributor

andrew-polk commented Feb 16, 2026

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.
We don't have things hooked up to make it automatic to report easily, but it found these. If you would like more detail on any of them, let me know.

https://app.devin.ai/review/sillsdev/HearThisAndroid/pull/13#issuecomment-3910931106

image

devin-ai-integration[bot]

This comment was marked as resolved.

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().
Copy link
Collaborator Author

@wmergenthal wmergenthal left a comment

Choose a reason for hiding this comment

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

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";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines +36 to +41
public synchronized void pet() {
if (watchdogTask != null && !watchdogTask.isDone()) {
watchdogTask.cancel(false);
}
watchdogTask = scheduler.schedule(onTimeout, timeout, unit);
}

Choose a reason for hiding this comment

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

🔴 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:

  1. Watchdog scheduler thread: timeout fires → onTimeout runs → calls listener.onNotification("sync_error")SyncActivity.onNotification() at line 301 calls watchdog.shutdown()
  2. SyncServer thread (concurrently): handling a file request → calls receivingFile() at line 346 → watchdog.pet()scheduler.schedule() throws RejectedExecutionException

Alternatively, a normal sync completion path has the same issue: AcceptNotificationHandler.handle() notifies listeners → onNotificationwatchdog.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.

Suggested change
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);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +79 to +83
}

if (status == null) {
// We got something but it wasn't "status". Make sure the user sees an error message.
status = "sync_error";

Choose a reason for hiding this comment

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

🔴 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:

  1. uri.getQuery() returns null for /notify (no query string)
  2. The for-loop is skipped entirely
  3. status remains null
  4. Line 81-83 sets status = "sync_error"
  5. 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";
    }
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

3 participants