From 5f5264387a27eb05eb1687c5a138450c74c00ac2 Mon Sep 17 00:00:00 2001 From: Wade Mergenthal Date: Wed, 27 Aug 2025 17:26:57 -0400 Subject: [PATCH 1/8] Handle both 'sync_success' and 'sync_interrupted' statuses from HT instead 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. --- .../org/sil/hearthis/AcceptFileHandler.java | 6 +- .../hearthis/AcceptNotificationHandler.java | 24 ++++- .../org/sil/hearthis/RequestFileHandler.java | 6 +- .../java/org/sil/hearthis/SyncActivity.java | 102 +++++++++++++----- .../java/org/sil/hearthis/SyncServer.java | 6 ++ .../java/org/sil/hearthis/SyncService.java | 6 ++ app/src/main/res/values/strings.xml | 1 + 7 files changed, 118 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java b/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java index 96f0cd8..8ad9bed 100644 --- a/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java +++ b/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java @@ -2,6 +2,7 @@ import android.content.Context; import android.net.Uri; +import android.util.Log; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; @@ -32,8 +33,10 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC File baseDir = _parent.getExternalFilesDir(null); Uri uri = Uri.parse(request.getRequestLine().getUri()); String filePath = uri.getQueryParameter("path"); - if (listener != null) + if (listener != null) { + Log.d("Sync", "AcceptFileHandler, listener.receivingFile() for " + filePath); listener.receivingFile(filePath); + } String path = baseDir + "/" + filePath; HttpEntity entity = null; String result = "failure"; @@ -63,6 +66,7 @@ public interface IFileReceivedNotification { static IFileReceivedNotification listener; public static void requestFileReceivedNotification(IFileReceivedNotification newListener) { + Log.d("Sync", "AcceptFileHandler, instantiating listener"); listener = newListener; // We only support notifying the most recent for now. } } diff --git a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java index b9b3250..f301e62 100644 --- a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java +++ b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java @@ -1,5 +1,6 @@ package org.sil.hearthis; import android.content.Context; +import android.util.Log; import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -32,9 +33,28 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC // Enhance: allow the notification to contain a message, and pass it on. // The copy is made because the onNotification calls may well remove listeners, leading to concurrent modification exceptions. + + // The notification received from HearThis is an HttpRequest, like this: + // POST /notify?message=sync_success HTTP/1.1 + // Per RFC 7230, parse the HttpRequest to extract the "message" portion that conveys status, + // and send to UI. + // NOTE: like several things in HearThisAndroid, HttpRequest is deprecated. It will be replaced + // with something more appropriate in a subsequent pull request, hopefully soon. + + String s1 = request.getRequestLine().toString(); + Log.d("Sync", "AcceptNotificationHandler, parsing request.getRequestLine()"); + Log.d("Sync", " s1 = " + s1); + // We want the part between the two space chars, after the '='. + String s2 = s1.substring(s1.indexOf(' ') + 1, s1.lastIndexOf(' ')); + Log.d("Sync", " s2 = " + s2); + String status = s2.substring(s2.indexOf('=') + 1); + Log.d("Sync", "AcceptNotificationHandler, status = " + status); + for (NotificationListener listener: notificationListeners.toArray(new NotificationListener[notificationListeners.size()])) { - listener.onNotification(""); + //listener.onNotification(""); + listener.onNotification(status); } - response.setEntity(new StringEntity("success")); + //response.setEntity(new StringEntity("success")); + response.setEntity(new StringEntity(status)); } } diff --git a/app/src/main/java/org/sil/hearthis/RequestFileHandler.java b/app/src/main/java/org/sil/hearthis/RequestFileHandler.java index a5bae8a..f942b42 100644 --- a/app/src/main/java/org/sil/hearthis/RequestFileHandler.java +++ b/app/src/main/java/org/sil/hearthis/RequestFileHandler.java @@ -2,6 +2,7 @@ import android.content.Context; import android.net.Uri; +import android.util.Log; import org.apache.http.HttpException; import org.apache.http.HttpRequest; @@ -29,8 +30,10 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC File baseDir = _parent.getExternalFilesDir(null); Uri uri = Uri.parse(request.getRequestLine().getUri()); String filePath = uri.getQueryParameter("path"); - if (listener!= null) + if (listener!= null) { + Log.d("Sync", "RequestFileHandler, listener.sendingFile() for " + filePath); listener.sendingFile(filePath); + } String path = baseDir + "/" + filePath; File file = new File(path); if (!file.exists()) { @@ -50,6 +53,7 @@ public interface IFileSentNotification { static IFileSentNotification listener; public static void requestFileSentNotification(IFileSentNotification newListener) { + Log.d("Sync", "RequestFileHandler, instantiating listener"); listener = newListener; // We only support notifying the most recent for now. } } diff --git a/app/src/main/java/org/sil/hearthis/SyncActivity.java b/app/src/main/java/org/sil/hearthis/SyncActivity.java index 2f8bbed..e1ac7d9 100644 --- a/app/src/main/java/org/sil/hearthis/SyncActivity.java +++ b/app/src/main/java/org/sil/hearthis/SyncActivity.java @@ -4,12 +4,15 @@ import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageManager; -import android.os.AsyncTask; +//import android.os.AsyncTask; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; import android.util.SparseArray; import android.view.Menu; import android.view.MenuItem; @@ -32,6 +35,8 @@ import java.net.UnknownHostException; import java.util.Date; import java.util.Enumeration; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; public class SyncActivity extends AppCompatActivity implements AcceptNotificationHandler.NotificationListener, @@ -55,6 +60,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sync); getSupportActionBar().setTitle(R.string.sync_title); + Log.d("Sync", "onCreate, calling startSyncServer()"); startSyncServer(); progressView = (TextView) findViewById(R.id.progress); continueButton = (Button) findViewById(R.id.continue_button); @@ -73,12 +79,15 @@ public void onClick(View view) { private void startSyncServer() { Intent serviceIntent = new Intent(this, SyncService.class); startService(serviceIntent); + Log.d("Sync", "startSyncServer, started service"); } @Override protected void onResume() { super.onResume(); + Log.d("Sync", "onResume, calling AcceptFileHandler.requestFileReceivedNotification()"); AcceptFileHandler.requestFileReceivedNotification(this); + Log.d("Sync", "onResume, calling RequestFileHandler.requestFileSentNotification()"); RequestFileHandler.requestFileSentNotification((this)); } @@ -125,6 +134,8 @@ public void release() { // Toast.makeText(getApplicationContext(), "To prevent memory leaks barcode scanner has been stopped", Toast.LENGTH_SHORT).show(); } + // Replacing 'AsyncTask' with 'Executors' and 'Handlers' in this method is based on: + // https://stackoverflow.com/questions/58767733/the-asynctask-api-is-deprecated-in-android-11-what-are-the-alternatives @Override public void receiveDetections(Detector.Detections detections) { final SparseArray barcodes = detections.getDetectedItems(); @@ -145,15 +156,37 @@ public void run() { // provide some users a clue that all is not well. ipView.setText(contents); preview.setVisibility(View.INVISIBLE); - SendMessage sendMessageTask = new SendMessage(); - sendMessageTask.ourIpAddress = getOurIpAddress(); - sendMessageTask.execute(); + //SendMessage sendMessageTask = new SendMessage(); + //sendMessageTask.ourIpAddress = getOurIpAddress(); + //sendMessageTask.execute(); + ExecutorService executor = Executors.newSingleThreadExecutor(); + Handler handler = new Handler(Looper.getMainLooper()); + executor.execute(() -> { + // Background work: send UDP packet to IP address given in the QR code. + try { + String ourIpAddress = getOurIpAddress(); + Log.d("Sync", "local IP address = " + ourIpAddress); + String ipAddress = ipView.getText().toString(); + Log.d("Sync", "remote IP address = " + ipAddress); + InetAddress receiverAddress = InetAddress.getByName(ipAddress); + DatagramSocket socket = new DatagramSocket(); + byte[] ipBytes = ourIpAddress.getBytes("UTF-8"); + DatagramPacket packet = new DatagramPacket(ipBytes, ipBytes.length, receiverAddress, desktopPort); + socket.send(packet); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + handler.post(() -> { + // Background work done, no foreground/UI work needed. + }); + }); cameraSource.stop(); cameraSource.release(); cameraSource = null; } }); - } } } @@ -216,11 +249,8 @@ private String getOurIpAddress() { if (inetAddress.isSiteLocalAddress()) { return inetAddress.getHostAddress(); } - } - } - } catch (SocketException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -247,17 +277,29 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onNotification(String message) { + Log.d("Sync", "onNotification, called with " + message); AcceptNotificationHandler.removeNotificationListener(this); - setProgress(getString(R.string.sync_success)); + //setProgress(getString(R.string.sync_success)); + if (message.equals("sync_success")) { + Log.d("Sync", "onNotification, calling setProgress(sync_success)"); + setProgress(getString(R.string.sync_success)); + } else if (message.equals("sync_interrupted")) { + Log.d("Sync", "onNotification, calling setProgress(sync_interrupted)"); + setProgress(getString(R.string.sync_interrupted)); + } else { + Log.d("Sync", "onNotification, illegal message: " + message); + } runOnUiThread(new Runnable() { @Override public void run() { continueButton.setEnabled(true); + Log.d("Sync", "onNotification, continue button enabled"); } }); } void setProgress(final String text) { + Log.d("Sync", "setProgress, called with " + text); runOnUiThread(new Runnable() { public void run() { progressView.setText(text); @@ -276,6 +318,7 @@ public void receivingFile(final String name) { return; lastProgress = new Date(); setProgress("receiving " + name); + Log.d("Sync", "receivingFile, name = " + name); } @Override @@ -284,28 +327,29 @@ public void sendingFile(final String name) { return; lastProgress = new Date(); setProgress("sending " + name); + Log.d("Sync", "sendingFile, name = " + name); } // This class is responsible to send one message packet to the IP address we // obtained from the desktop, containing the Android's own IP address. - private class SendMessage extends AsyncTask { - - public String ourIpAddress; - @Override - protected Void doInBackground(Void... params) { - try { - String ipAddress = ipView.getText().toString(); - InetAddress receiverAddress = InetAddress.getByName(ipAddress); - DatagramSocket socket = new DatagramSocket(); - byte[] buffer = ourIpAddress.getBytes("UTF-8"); - DatagramPacket packet = new DatagramPacket(buffer, buffer.length, receiverAddress, desktopPort); - socket.send(packet); - } catch (UnknownHostException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return null; - } - } + //private class SendMessage extends AsyncTask { + // + // public String ourIpAddress; + // @Override + // protected Void doInBackground(Void... params) { + // try { + // String ipAddress = ipView.getText().toString(); + // InetAddress receiverAddress = InetAddress.getByName(ipAddress); + // DatagramSocket socket = new DatagramSocket(); + // byte[] buffer = ourIpAddress.getBytes("UTF-8"); + // DatagramPacket packet = new DatagramPacket(buffer, buffer.length, receiverAddress, desktopPort); + // socket.send(packet); + // } catch (UnknownHostException e) { + // e.printStackTrace(); + // } catch (IOException e) { + // e.printStackTrace(); + // } + // return null; + // } + //} } diff --git a/app/src/main/java/org/sil/hearthis/SyncServer.java b/app/src/main/java/org/sil/hearthis/SyncServer.java index f97e21e..2891306 100644 --- a/app/src/main/java/org/sil/hearthis/SyncServer.java +++ b/app/src/main/java/org/sil/hearthis/SyncServer.java @@ -1,5 +1,6 @@ package org.sil.hearthis; +import android.util.Log; import org.apache.http.HttpException; import org.apache.http.impl.DefaultConnectionReuseStrategy; import org.apache.http.impl.DefaultHttpResponseFactory; @@ -69,6 +70,7 @@ public synchronized void startThread() { return; // already started, must not do twice. _running = true; + Log.d("Sync", "SyncServer, calling super.start()"); super.start(); } @@ -80,6 +82,7 @@ public synchronized void stopThread(){ // Method executed in thread when super.start() is called. @Override public void run() { + Log.d("Sync", "SyncServer, calling super.run()"); super.run(); try { @@ -95,8 +98,10 @@ public void run() { serverConnection.bind(socket, new BasicHttpParams()); + //Log.d("Sync", "SyncServer.run, handling request"); httpService.handleRequest(serverConnection, httpContext); + //Log.d("Sync", "SyncServer.run, shutting down"); serverConnection.shutdown(); } catch (IOException e) { e.printStackTrace(); @@ -105,6 +110,7 @@ public void run() { } } + Log.d("Sync", "SyncServer.run, closing serverSocket"); serverSocket.close(); } catch (IOException e) { diff --git a/app/src/main/java/org/sil/hearthis/SyncService.java b/app/src/main/java/org/sil/hearthis/SyncService.java index f7647f3..a8a955c 100644 --- a/app/src/main/java/org/sil/hearthis/SyncService.java +++ b/app/src/main/java/org/sil/hearthis/SyncService.java @@ -3,6 +3,7 @@ import android.app.Service; import android.content.Intent; import android.os.IBinder; +import android.util.Log; // Service that runs a simple 'web server' that HearThis desktop can talk to. public class SyncService extends Service { @@ -18,20 +19,25 @@ public IBinder onBind(Intent intent) { @Override public void onCreate() { + Log.d("Sync", "SyncService, calling super.onCreate()"); super.onCreate(); + Log.d("Sync", "SyncService, _server = new SyncServer"); _server = new SyncServer(this); } @Override public void onDestroy() { + Log.d("Sync", "SyncService, calling _server.stopThread()"); _server.stopThread(); + Log.d("Sync", "SyncService, calling super.onDestroy()"); super.onDestroy(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { + Log.d("Sync", "SyncService, calling _server.startThread()"); _server.startThread(); return START_STICKY; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 471d647..786ff72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Continue ChooseBookActivity Sync completed successfully! + Sync was interrupted or cancelled Choose a project Choose a book Choose a chapter From fd35b080e346d41d75e8425714fd7401daa77b5c Mon Sep 17 00:00:00 2001 From: Wade Mergenthal Date: Thu, 28 Aug 2025 17:02:54 -0400 Subject: [PATCH 2/8] HT-508: finish HTA-side implementation of sync-interrupted scenario. 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'. --- .../org/sil/hearthis/AcceptFileHandler.java | 6 +-- .../hearthis/AcceptNotificationHandler.java | 21 +++----- .../org/sil/hearthis/RequestFileHandler.java | 6 +-- .../java/org/sil/hearthis/SyncActivity.java | 54 ++++--------------- .../java/org/sil/hearthis/SyncServer.java | 6 --- .../java/org/sil/hearthis/SyncService.java | 6 --- 6 files changed, 19 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java b/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java index 8ad9bed..96f0cd8 100644 --- a/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java +++ b/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java @@ -2,7 +2,6 @@ import android.content.Context; import android.net.Uri; -import android.util.Log; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; @@ -33,10 +32,8 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC File baseDir = _parent.getExternalFilesDir(null); Uri uri = Uri.parse(request.getRequestLine().getUri()); String filePath = uri.getQueryParameter("path"); - if (listener != null) { - Log.d("Sync", "AcceptFileHandler, listener.receivingFile() for " + filePath); + if (listener != null) listener.receivingFile(filePath); - } String path = baseDir + "/" + filePath; HttpEntity entity = null; String result = "failure"; @@ -66,7 +63,6 @@ public interface IFileReceivedNotification { static IFileReceivedNotification listener; public static void requestFileReceivedNotification(IFileReceivedNotification newListener) { - Log.d("Sync", "AcceptFileHandler, instantiating listener"); listener = newListener; // We only support notifying the most recent for now. } } diff --git a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java index f301e62..5de015e 100644 --- a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java +++ b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java @@ -1,6 +1,5 @@ package org.sil.hearthis; import android.content.Context; -import android.util.Log; import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -34,27 +33,23 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC // Enhance: allow the notification to contain a message, and pass it on. // The copy is made because the onNotification calls may well remove listeners, leading to concurrent modification exceptions. - // The notification received from HearThis is an HttpRequest, like this: + // HT-508: to prevent HTA from getting stuck in a bad state when sync is interrupted, + // extract and handle sync status that HT inserted into the notification. + // The notification received from the HearThis PC is an HttpRequest (RFC 7230), like this: // POST /notify?message=sync_success HTTP/1.1 - // Per RFC 7230, parse the HttpRequest to extract the "message" portion that conveys status, - // and send to UI. - // NOTE: like several things in HearThisAndroid, HttpRequest is deprecated. It will be replaced - // with something more appropriate in a subsequent pull request, hopefully soon. + // Sync status is in the "message" portion. Extract it and send it along. + // + // NOTE: like several things in HearThisAndroid, HttpRequest is deprecated. It will be + // replaced with something more appropriate, hopefully soon. String s1 = request.getRequestLine().toString(); - Log.d("Sync", "AcceptNotificationHandler, parsing request.getRequestLine()"); - Log.d("Sync", " s1 = " + s1); - // We want the part between the two space chars, after the '='. + // We want the part between the two space chars, and after the '=' String s2 = s1.substring(s1.indexOf(' ') + 1, s1.lastIndexOf(' ')); - Log.d("Sync", " s2 = " + s2); String status = s2.substring(s2.indexOf('=') + 1); - Log.d("Sync", "AcceptNotificationHandler, status = " + status); for (NotificationListener listener: notificationListeners.toArray(new NotificationListener[notificationListeners.size()])) { - //listener.onNotification(""); listener.onNotification(status); } - //response.setEntity(new StringEntity("success")); response.setEntity(new StringEntity(status)); } } diff --git a/app/src/main/java/org/sil/hearthis/RequestFileHandler.java b/app/src/main/java/org/sil/hearthis/RequestFileHandler.java index f942b42..a5bae8a 100644 --- a/app/src/main/java/org/sil/hearthis/RequestFileHandler.java +++ b/app/src/main/java/org/sil/hearthis/RequestFileHandler.java @@ -2,7 +2,6 @@ import android.content.Context; import android.net.Uri; -import android.util.Log; import org.apache.http.HttpException; import org.apache.http.HttpRequest; @@ -30,10 +29,8 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC File baseDir = _parent.getExternalFilesDir(null); Uri uri = Uri.parse(request.getRequestLine().getUri()); String filePath = uri.getQueryParameter("path"); - if (listener!= null) { - Log.d("Sync", "RequestFileHandler, listener.sendingFile() for " + filePath); + if (listener!= null) listener.sendingFile(filePath); - } String path = baseDir + "/" + filePath; File file = new File(path); if (!file.exists()) { @@ -53,7 +50,6 @@ public interface IFileSentNotification { static IFileSentNotification listener; public static void requestFileSentNotification(IFileSentNotification newListener) { - Log.d("Sync", "RequestFileHandler, instantiating listener"); listener = newListener; // We only support notifying the most recent for now. } } diff --git a/app/src/main/java/org/sil/hearthis/SyncActivity.java b/app/src/main/java/org/sil/hearthis/SyncActivity.java index e1ac7d9..d514618 100644 --- a/app/src/main/java/org/sil/hearthis/SyncActivity.java +++ b/app/src/main/java/org/sil/hearthis/SyncActivity.java @@ -4,7 +4,6 @@ import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageManager; -//import android.os.AsyncTask; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; @@ -12,7 +11,6 @@ import android.os.Handler; import android.os.Looper; -import android.util.Log; import android.util.SparseArray; import android.view.Menu; import android.view.MenuItem; @@ -60,7 +58,6 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sync); getSupportActionBar().setTitle(R.string.sync_title); - Log.d("Sync", "onCreate, calling startSyncServer()"); startSyncServer(); progressView = (TextView) findViewById(R.id.progress); continueButton = (Button) findViewById(R.id.continue_button); @@ -79,15 +76,12 @@ public void onClick(View view) { private void startSyncServer() { Intent serviceIntent = new Intent(this, SyncService.class); startService(serviceIntent); - Log.d("Sync", "startSyncServer, started service"); } @Override protected void onResume() { super.onResume(); - Log.d("Sync", "onResume, calling AcceptFileHandler.requestFileReceivedNotification()"); AcceptFileHandler.requestFileReceivedNotification(this); - Log.d("Sync", "onResume, calling RequestFileHandler.requestFileSentNotification()"); RequestFileHandler.requestFileSentNotification((this)); } @@ -134,7 +128,7 @@ public void release() { // Toast.makeText(getApplicationContext(), "To prevent memory leaks barcode scanner has been stopped", Toast.LENGTH_SHORT).show(); } - // Replacing 'AsyncTask' with 'Executors' and 'Handlers' in this method is based on: + // Replacing 'AsyncTask' (deprecated) with 'Executors' and 'Handlers' in this method is inspired by: // https://stackoverflow.com/questions/58767733/the-asynctask-api-is-deprecated-in-android-11-what-are-the-alternatives @Override public void receiveDetections(Detector.Detections detections) { @@ -156,18 +150,13 @@ public void run() { // provide some users a clue that all is not well. ipView.setText(contents); preview.setVisibility(View.INVISIBLE); - //SendMessage sendMessageTask = new SendMessage(); - //sendMessageTask.ourIpAddress = getOurIpAddress(); - //sendMessageTask.execute(); ExecutorService executor = Executors.newSingleThreadExecutor(); Handler handler = new Handler(Looper.getMainLooper()); executor.execute(() -> { // Background work: send UDP packet to IP address given in the QR code. try { String ourIpAddress = getOurIpAddress(); - Log.d("Sync", "local IP address = " + ourIpAddress); String ipAddress = ipView.getText().toString(); - Log.d("Sync", "remote IP address = " + ipAddress); InetAddress receiverAddress = InetAddress.getByName(ipAddress); DatagramSocket socket = new DatagramSocket(); byte[] ipBytes = ourIpAddress.getBytes("UTF-8"); @@ -179,7 +168,7 @@ public void run() { e.printStackTrace(); } handler.post(() -> { - // Background work done, no foreground/UI work needed. + // Background work done, no associated foreground work needed. }); }); cameraSource.stop(); @@ -277,29 +266,29 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onNotification(String message) { - Log.d("Sync", "onNotification, called with " + message); AcceptNotificationHandler.removeNotificationListener(this); - //setProgress(getString(R.string.sync_success)); + + // HT-508: HearThis PC now includes sync status in its notification to the app. + // Possible sync statuses recognized: + // - success + // - interrupted; handling this should prevent the app from getting into a bad state. if (message.equals("sync_success")) { - Log.d("Sync", "onNotification, calling setProgress(sync_success)"); setProgress(getString(R.string.sync_success)); } else if (message.equals("sync_interrupted")) { - Log.d("Sync", "onNotification, calling setProgress(sync_interrupted)"); setProgress(getString(R.string.sync_interrupted)); } else { - Log.d("Sync", "onNotification, illegal message: " + message); + // Should never happen. Not sure what to do here, if anything... + //Log.d("Sync", "onNotification, illegal message: " + message); } runOnUiThread(new Runnable() { @Override public void run() { continueButton.setEnabled(true); - Log.d("Sync", "onNotification, continue button enabled"); } }); } void setProgress(final String text) { - Log.d("Sync", "setProgress, called with " + text); runOnUiThread(new Runnable() { public void run() { progressView.setText(text); @@ -318,7 +307,6 @@ public void receivingFile(final String name) { return; lastProgress = new Date(); setProgress("receiving " + name); - Log.d("Sync", "receivingFile, name = " + name); } @Override @@ -327,29 +315,5 @@ public void sendingFile(final String name) { return; lastProgress = new Date(); setProgress("sending " + name); - Log.d("Sync", "sendingFile, name = " + name); } - - // This class is responsible to send one message packet to the IP address we - // obtained from the desktop, containing the Android's own IP address. - //private class SendMessage extends AsyncTask { - // - // public String ourIpAddress; - // @Override - // protected Void doInBackground(Void... params) { - // try { - // String ipAddress = ipView.getText().toString(); - // InetAddress receiverAddress = InetAddress.getByName(ipAddress); - // DatagramSocket socket = new DatagramSocket(); - // byte[] buffer = ourIpAddress.getBytes("UTF-8"); - // DatagramPacket packet = new DatagramPacket(buffer, buffer.length, receiverAddress, desktopPort); - // socket.send(packet); - // } catch (UnknownHostException e) { - // e.printStackTrace(); - // } catch (IOException e) { - // e.printStackTrace(); - // } - // return null; - // } - //} } diff --git a/app/src/main/java/org/sil/hearthis/SyncServer.java b/app/src/main/java/org/sil/hearthis/SyncServer.java index 2891306..f97e21e 100644 --- a/app/src/main/java/org/sil/hearthis/SyncServer.java +++ b/app/src/main/java/org/sil/hearthis/SyncServer.java @@ -1,6 +1,5 @@ package org.sil.hearthis; -import android.util.Log; import org.apache.http.HttpException; import org.apache.http.impl.DefaultConnectionReuseStrategy; import org.apache.http.impl.DefaultHttpResponseFactory; @@ -70,7 +69,6 @@ public synchronized void startThread() { return; // already started, must not do twice. _running = true; - Log.d("Sync", "SyncServer, calling super.start()"); super.start(); } @@ -82,7 +80,6 @@ public synchronized void stopThread(){ // Method executed in thread when super.start() is called. @Override public void run() { - Log.d("Sync", "SyncServer, calling super.run()"); super.run(); try { @@ -98,10 +95,8 @@ public void run() { serverConnection.bind(socket, new BasicHttpParams()); - //Log.d("Sync", "SyncServer.run, handling request"); httpService.handleRequest(serverConnection, httpContext); - //Log.d("Sync", "SyncServer.run, shutting down"); serverConnection.shutdown(); } catch (IOException e) { e.printStackTrace(); @@ -110,7 +105,6 @@ public void run() { } } - Log.d("Sync", "SyncServer.run, closing serverSocket"); serverSocket.close(); } catch (IOException e) { diff --git a/app/src/main/java/org/sil/hearthis/SyncService.java b/app/src/main/java/org/sil/hearthis/SyncService.java index a8a955c..f7647f3 100644 --- a/app/src/main/java/org/sil/hearthis/SyncService.java +++ b/app/src/main/java/org/sil/hearthis/SyncService.java @@ -3,7 +3,6 @@ import android.app.Service; import android.content.Intent; import android.os.IBinder; -import android.util.Log; // Service that runs a simple 'web server' that HearThis desktop can talk to. public class SyncService extends Service { @@ -19,25 +18,20 @@ public IBinder onBind(Intent intent) { @Override public void onCreate() { - Log.d("Sync", "SyncService, calling super.onCreate()"); super.onCreate(); - Log.d("Sync", "SyncService, _server = new SyncServer"); _server = new SyncServer(this); } @Override public void onDestroy() { - Log.d("Sync", "SyncService, calling _server.stopThread()"); _server.stopThread(); - Log.d("Sync", "SyncService, calling super.onDestroy()"); super.onDestroy(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { - Log.d("Sync", "SyncService, calling _server.startThread()"); _server.startThread(); return START_STICKY; } From 3727d832e3f06318392afaf0f9baad995b830aa4 Mon Sep 17 00:00:00 2001 From: Wade Mergenthal Date: Mon, 8 Sep 2025 15:08:40 -0400 Subject: [PATCH 3/8] Sync final status extracted from HT's notification with greater clarity Instead of raw string objects, use a URI object for status extraction from the HttpRequest from HT. Resulting logic is longer but clearer. --- .../hearthis/AcceptNotificationHandler.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java index 5de015e..0b3af54 100644 --- a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java +++ b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java @@ -7,6 +7,7 @@ import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestHandler; import java.io.IOException; +import java.net.URI; import java.util.ArrayList; /** @@ -42,10 +43,26 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC // NOTE: like several things in HearThisAndroid, HttpRequest is deprecated. It will be // replaced with something more appropriate, hopefully soon. - String s1 = request.getRequestLine().toString(); - // We want the part between the two space chars, and after the '=' - String s2 = s1.substring(s1.indexOf(' ') + 1, s1.lastIndexOf(' ')); - String status = s2.substring(s2.indexOf('=') + 1); + String status = null; + try { + String s1 = request.getRequestLine().getUri(); + URI uri = new URI(s1); + String query = uri.getQuery(); + for (String param : query.split("&")) { + String[] pair = param.split("="); + if (pair.length == 2 && pair[0].equals("message")) { + status = pair[1]; + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + if (status == null) { + // Something went wrong. Make sure the user sees a non-success message. + status = "sync_interrupted"; + } for (NotificationListener listener: notificationListeners.toArray(new NotificationListener[notificationListeners.size()])) { listener.onNotification(status); From 399e532756e507f080e1ee7d26d6b8e3c32d00f0 Mon Sep 17 00:00:00 2001 From: Wade Mergenthal Date: Mon, 8 Sep 2025 15:34:22 -0400 Subject: [PATCH 4/8] Handle case when HTA does not understand sync final status from HT 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. --- app/src/main/java/org/sil/hearthis/SyncActivity.java | 4 ++-- app/src/main/res/values/strings.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/sil/hearthis/SyncActivity.java b/app/src/main/java/org/sil/hearthis/SyncActivity.java index d514618..0944a98 100644 --- a/app/src/main/java/org/sil/hearthis/SyncActivity.java +++ b/app/src/main/java/org/sil/hearthis/SyncActivity.java @@ -277,8 +277,8 @@ public void onNotification(String message) { } else if (message.equals("sync_interrupted")) { setProgress(getString(R.string.sync_interrupted)); } else { - // Should never happen. Not sure what to do here, if anything... - //Log.d("Sync", "onNotification, illegal message: " + message); + // Should not happen. Likely caused by incompatible versions of HT/HTA. Warn the user. + setProgress(getString(R.string.sync_suspect)); } runOnUiThread(new Runnable() { @Override diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 786ff72..efd5a85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ ChooseBookActivity Sync completed successfully! Sync was interrupted or cancelled + Sync status unknown. Please ensure that HearThis and HearThisAndroid versions are compatible. Choose a project Choose a book Choose a chapter From 678cd7c4f3eff68ce355d3e5b289ceef18e1ab5b Mon Sep 17 00:00:00 2001 From: Wade Mergenthal Date: Tue, 30 Sep 2025 21:58:58 -0400 Subject: [PATCH 5/8] Safety checkin: clearer status extract, add watchdog and debug output 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). --- .../org/sil/hearthis/AcceptFileHandler.java | 5 +- .../hearthis/AcceptNotificationHandler.java | 48 +++++++--- .../org/sil/hearthis/RequestFileHandler.java | 5 +- .../java/org/sil/hearthis/SyncActivity.java | 90 +++++++++++++++---- .../java/org/sil/hearthis/SyncServer.java | 9 ++ .../main/java/org/sil/hearthis/Watchdog.java | 58 ++++++++++++ app/src/main/res/values/strings.xml | 2 +- 7 files changed, 188 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/org/sil/hearthis/Watchdog.java diff --git a/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java b/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java index 96f0cd8..1c9d304 100644 --- a/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java +++ b/app/src/main/java/org/sil/hearthis/AcceptFileHandler.java @@ -2,6 +2,7 @@ import android.content.Context; import android.net.Uri; +import android.util.Log; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; @@ -32,8 +33,10 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC File baseDir = _parent.getExternalFilesDir(null); Uri uri = Uri.parse(request.getRequestLine().getUri()); String filePath = uri.getQueryParameter("path"); - if (listener != null) + if (listener != null) { + Log.d("Sync", "AcceptFileHandler, calling listener.receivingFile(" + filePath + ")"); listener.receivingFile(filePath); + } String path = baseDir + "/" + filePath; HttpEntity entity = null; String result = "failure"; diff --git a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java index 0b3af54..ce45323 100644 --- a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java +++ b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java @@ -1,5 +1,7 @@ package org.sil.hearthis; import android.content.Context; +import android.util.Log; // WM, TEMPORARY! + import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -15,6 +17,8 @@ */ public class AcceptNotificationHandler implements HttpRequestHandler { + private static String minHtaVersion = null; + public interface NotificationListener { void onNotification(String message); } @@ -35,38 +39,62 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC // The copy is made because the onNotification calls may well remove listeners, leading to concurrent modification exceptions. // HT-508: to prevent HTA from getting stuck in a bad state when sync is interrupted, - // extract and handle sync status that HT inserted into the notification. - // The notification received from the HearThis PC is an HttpRequest (RFC 7230), like this: - // POST /notify?message=sync_success HTTP/1.1 - // Sync status is in the "message" portion. Extract it and send it along. + // extract and handle sync status that HT inserted into the notification. HT also sets up + // that notification by first sending a notification containing the minimum HTA version + // needed for this exchange. + // The notifications received from the HearThis PC are HttpRequest (RFC 7230), like this: + // POST /notify?minHtaVersion=1.0 HTTP/1.1 -- HT sends this first + // POST /notify?status=sync_success HTTP/1.1 -- HT sends this second + // Payload is in the portion after the 'notify'. Extract it and send it along. + // If something goes wrong and that is not possible, send along an error indication. + // NOTIFICATION ORDER IS IMPORTANT. HT must send the HTA version info first, and then the + // sync final status. This is enforced by an early return when the HTA version info is seen. // // NOTE: like several things in HearThisAndroid, HttpRequest is deprecated. It will be // replaced with something more appropriate, hopefully soon. String status = null; + Log.d("Sync", "handle, begin, minHtaVersion = " + minHtaVersion); // WM, TEMPORARY try { String s1 = request.getRequestLine().getUri(); URI uri = new URI(s1); String query = uri.getQuery(); - for (String param : query.split("&")) { - String[] pair = param.split("="); - if (pair.length == 2 && pair[0].equals("message")) { - status = pair[1]; - break; + Log.d("Sync", "handle, query = " + query); // WM, TEMPORARY + 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("status")) { + status = pair[1]; + Log.d("Sync", "handle, status = " + status); // WM, TEMPORARY + } else if (pair[0].equals("minHtaVersion")) { + minHtaVersion = pair[1]; + Log.d("Sync", "handle, minHtaVersion = " + minHtaVersion + ", returning"); // WM, TEMPORARY + return; + } + } } } + Log.d("Sync", "handle, results: status = " + status + ", minHtaVersion = " + minHtaVersion); // WM, TEMPORARY } catch (Exception e) { e.printStackTrace(); } if (status == null) { // Something went wrong. Make sure the user sees a non-success message. - status = "sync_interrupted"; + //if (minHtaVersion != null) { + // status = "sync_unknown"; // error also: we got something but it wasn't "status" + //} else { + status = "sync_error"; + //} } + Log.d("Sync", "handle, final from HT, status = " + status); // WM, TEMPORARY for (NotificationListener listener: notificationListeners.toArray(new NotificationListener[notificationListeners.size()])) { + Log.d("Sync", "handle, calling listener.onNotification(" + status + ")"); // WM, TEMPORARY listener.onNotification(status); } + Log.d("Sync", "handle, putting status in response (I think)"); // WM, TEMPORARY response.setEntity(new StringEntity(status)); } } diff --git a/app/src/main/java/org/sil/hearthis/RequestFileHandler.java b/app/src/main/java/org/sil/hearthis/RequestFileHandler.java index a5bae8a..81055db 100644 --- a/app/src/main/java/org/sil/hearthis/RequestFileHandler.java +++ b/app/src/main/java/org/sil/hearthis/RequestFileHandler.java @@ -2,6 +2,7 @@ import android.content.Context; import android.net.Uri; +import android.util.Log; import org.apache.http.HttpException; import org.apache.http.HttpRequest; @@ -29,8 +30,10 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC File baseDir = _parent.getExternalFilesDir(null); Uri uri = Uri.parse(request.getRequestLine().getUri()); String filePath = uri.getQueryParameter("path"); - if (listener!= null) + if (listener!= null) { + Log.d("Sync", "RequestFileHandler, calling listener.sendingFile(" + filePath + ")"); listener.sendingFile(filePath); + } String path = baseDir + "/" + filePath; File file = new File(path); if (!file.exists()) { diff --git a/app/src/main/java/org/sil/hearthis/SyncActivity.java b/app/src/main/java/org/sil/hearthis/SyncActivity.java index 0944a98..61bc90e 100644 --- a/app/src/main/java/org/sil/hearthis/SyncActivity.java +++ b/app/src/main/java/org/sil/hearthis/SyncActivity.java @@ -1,5 +1,8 @@ package org.sil.hearthis; +import static org.sil.hearthis.AcceptNotificationHandler.notificationListeners; +//import org.sil.hearthis.Watchdog; + import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; @@ -11,6 +14,7 @@ import android.os.Handler; import android.os.Looper; +import android.util.Log; import android.util.SparseArray; import android.view.Menu; import android.view.MenuItem; @@ -24,17 +28,20 @@ import com.google.android.gms.vision.barcode.Barcode; import com.google.android.gms.vision.barcode.BarcodeDetector; +//import org.apache.http.entity.StringEntity; + import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; -import java.net.UnknownHostException; +//import java.net.UnknownHostException; import java.util.Date; import java.util.Enumeration; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; public class SyncActivity extends AppCompatActivity implements AcceptNotificationHandler.NotificationListener, @@ -47,9 +54,15 @@ public class SyncActivity extends AppCompatActivity implements AcceptNotificatio SurfaceView preview; int desktopPort = 11007; // port on which the desktop is listening for our IP address. private static final int REQUEST_CAMERA_PERMISSION = 201; + private static final int WATCHDOG_TIMEOUT_SECONDS = 10; boolean scanning = false; TextView progressView; + Watchdog watchdog = new Watchdog(WATCHDOG_TIMEOUT_SECONDS, TimeUnit.SECONDS, () -> { + Log.d("Sync", "Watchdog, TIMED OUT, setting Error"); + setProgress(getString(R.string.sync_error)); + }); + private BarcodeDetector barcodeDetector; private CameraSource cameraSource; @@ -156,16 +169,22 @@ public void run() { // Background work: send UDP packet to IP address given in the QR code. try { String ourIpAddress = getOurIpAddress(); + Log.d("Sync", "SyncActivity.run, ourIpAddress = " + ourIpAddress); // WM, TEMPORARY String ipAddress = ipView.getText().toString(); InetAddress receiverAddress = InetAddress.getByName(ipAddress); DatagramSocket socket = new DatagramSocket(); byte[] ipBytes = ourIpAddress.getBytes("UTF-8"); DatagramPacket packet = new DatagramPacket(ipBytes, ipBytes.length, receiverAddress, desktopPort); - socket.send(packet); - } catch (UnknownHostException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); + Log.d("Sync", "SyncActivity.run, sending UDP packet"); // WM, TEMPORARY + //throw new IOException("TEST HACK"); // WM, test only! + socket.send(packet); // WM, comment out if preceding throw(), a hack, is present + } catch (IOException ioe) { + // Note: this also catches UnknownHostException, a subclass of IOException + for (AcceptNotificationHandler.NotificationListener listener : notificationListeners.toArray(new AcceptNotificationHandler.NotificationListener[notificationListeners.size()])) { + listener.onNotification("sync_interrupted"); + } + Log.d("Sync", "SyncActivity.run, got exception: " + ioe); + ioe.printStackTrace(); } handler.post(() -> { // Background work done, no associated foreground work needed. @@ -198,6 +217,7 @@ public void run() { String ourIpAddress = getOurIpAddress(); TextView ourIpView = (TextView) findViewById(R.id.our_ip_address); ourIpView.setText(ourIpAddress); + Log.d("Sync", "onCreateOptionsMenu, calling addNotificationListener()"); // WM, TEMPORARY AcceptNotificationHandler.addNotificationListener(this); return true; } @@ -266,19 +286,49 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onNotification(String message) { + Log.d("Sync", "onNotification(" + message + "), calling removeNotificationListener()"); // WM, TEMPORARY AcceptNotificationHandler.removeNotificationListener(this); // HT-508: HearThis PC now includes sync status in its notification to the app. - // Possible sync statuses recognized: - // - success - // - interrupted; handling this should prevent the app from getting into a bad state. - if (message.equals("sync_success")) { - setProgress(getString(R.string.sync_success)); - } else if (message.equals("sync_interrupted")) { - setProgress(getString(R.string.sync_interrupted)); - } else { - // Should not happen. Likely caused by incompatible versions of HT/HTA. Warn the user. - setProgress(getString(R.string.sync_suspect)); + // Handling status here in HearThisAndroid should prevent the app from getting + // into a bad state, besides informing the user about whether sync succeeded. + switch (message) { + case "sync_success": + Log.d("Sync", "onNotification.success, shut down watchdog"); // WM, temporary + //watchdog.pet(); + watchdog.shutdown(); + setProgress(getString(R.string.sync_success)); + Log.d("Sync", "onNotification, sync_success"); // WM, TEMPORARY + break; + case "sync_interrupted": + Log.d("Sync", "onNotification.interrupted, shut down watchdog"); // WM, temporary + //watchdog.pet(); + watchdog.shutdown(); + // Sync was interrupted or cancelled. + setProgress(getString(R.string.sync_interrupted)); + Log.d("Sync", "onNotification, sync_interrupted"); // WM, TEMPORARY + break; + case "sync_error": + // Internal HTA error or incompatible versions of HT and HTA. + Log.d("Sync", "onNotification.error, shut down watchdog"); // WM, temporary + //watchdog.pet(); + watchdog.shutdown(); + setProgress(getString(R.string.sync_error)); + Log.d("Sync", "onNotification, sync_error"); // WM, TEMPORARY + break; + //case "sync_unknown": + // // Likely caused by incompatible versions of HT and HTA. + // setProgress(getString(R.string.sync_unknown)); + // Log.d("Sync", "onNotification, sync_unknown"); // WM, TEMPORARY + // break; + default: + // Should never happen. Raise an error. + Log.d("Sync", "onNotification.default, shut down watchdog"); // WM, temporary + //watchdog.pet(); + watchdog.shutdown(); + setProgress(getString(R.string.sync_error)); + Log.d("Sync", "onNotification, bad status: " + message); + break; } runOnUiThread(new Runnable() { @Override @@ -301,19 +351,27 @@ public void run() { @Override public void receivingFile(final String name) { + Log.d("Sync", "receivingFile, pet watchdog"); // WM, temporary + watchdog.pet(); + // To prevent excess flicker and wasting compute time on progress reports, // only change once per second. if (new Date().getTime() - lastProgress.getTime() < 1000) return; lastProgress = new Date(); setProgress("receiving " + name); + Log.d("Sync", "receivingFile: " + name); } @Override public void sendingFile(final String name) { + Log.d("Sync", "sendingFile, pet watchdog"); // WM, temporary + watchdog.pet(); + if (new Date().getTime() - lastProgress.getTime() < 1000) return; lastProgress = new Date(); setProgress("sending " + name); + Log.d("Sync", "sendingFile: " + name); } } diff --git a/app/src/main/java/org/sil/hearthis/SyncServer.java b/app/src/main/java/org/sil/hearthis/SyncServer.java index f97e21e..b865c83 100644 --- a/app/src/main/java/org/sil/hearthis/SyncServer.java +++ b/app/src/main/java/org/sil/hearthis/SyncServer.java @@ -1,5 +1,7 @@ package org.sil.hearthis; +import android.util.Log; + import org.apache.http.HttpException; import org.apache.http.impl.DefaultConnectionReuseStrategy; import org.apache.http.impl.DefaultHttpResponseFactory; @@ -94,9 +96,16 @@ public void run() { DefaultHttpServerConnection serverConnection = new DefaultHttpServerConnection(); serverConnection.bind(socket, new BasicHttpParams()); + //Log.d("Sync", "SyncServer.run, new serverConnection, timeout = " + serverConnection.getSocketTimeout()); // WM, TEMPORARY + //Log.d("Sync", "SyncServer.run, calling handleRequest()"); // WM, TEMPORARY + // Set a timeout so that HTA doesn't get stuck if HT can't finish a sync. + //serverConnection.setSocketTimeout(5000); // what's optimum? try 5 secs... + //Log.d("Sync", " new timeout = " + serverConnection.getSocketTimeout()); // WM, TEMPORARY httpService.handleRequest(serverConnection, httpContext); + //Log.d("Sync", "SyncServer.run, handleRequest done, calling serverConnection.shutdown()"); // WM, TEMPORARY + //Log.d("Sync", " "); // WM, TEMPORARY serverConnection.shutdown(); } catch (IOException e) { e.printStackTrace(); diff --git a/app/src/main/java/org/sil/hearthis/Watchdog.java b/app/src/main/java/org/sil/hearthis/Watchdog.java new file mode 100644 index 0000000..90eeada --- /dev/null +++ b/app/src/main/java/org/sil/hearthis/Watchdog.java @@ -0,0 +1,58 @@ +package org.sil.hearthis; + +import android.util.Log; + +//import org.apache.http.HttpException; +//import org.apache.http.impl.DefaultConnectionReuseStrategy; +//import org.apache.http.impl.DefaultHttpResponseFactory; +//import org.apache.http.impl.DefaultHttpServerConnection; +//import org.apache.http.params.BasicHttpParams; +//import org.apache.http.protocol.BasicHttpContext; +//import org.apache.http.protocol.BasicHttpProcessor; +//import org.apache.http.protocol.HttpRequestHandlerRegistry; +//import org.apache.http.protocol.HttpService; +//import org.apache.http.protocol.ResponseConnControl; +//import org.apache.http.protocol.ResponseContent; +//import org.apache.http.protocol.ResponseDate; +//import org.apache.http.protocol.ResponseServer; + +//import java.io.IOException; +//import java.net.ServerSocket; +//import java.net.Socket; +import java.util.concurrent.*; + +/** + * This class implements a timeout for the Android side of a HearThis sync operation. + * + */ + +public class Watchdog { + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture watchdogTask; + private final Runnable onTimeout; + private final long timeout; + private final TimeUnit unit; + + public Watchdog(long timeout, TimeUnit unit, Runnable onTimeout) { + Log.d("Sync", "Watchdog, constructor begin, timeout = " + timeout); // WM, temporary + Log.d("Sync", " unit = " + unit); // WM, temporary + this.timeout = timeout; + this.unit = unit; + this.onTimeout = onTimeout; + } + + // Call this whenever input is received + public synchronized void pet() { + if (watchdogTask != null && !watchdogTask.isDone()) { + Log.d("Sync", "Watchdog, pet, setting cancel false"); // WM, temporary + watchdogTask.cancel(false); + } + Log.d("Sync", "Watchdog, pet, calling scheduler.schedule()"); // WM, temporary + watchdogTask = scheduler.schedule(onTimeout, timeout, unit); + } + + public void shutdown() { + Log.d("Sync", "Watchdog, shutting down"); // WM, temporary + scheduler.shutdownNow(); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index efd5a85..ba9b046 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,7 +18,7 @@ ChooseBookActivity Sync completed successfully! Sync was interrupted or cancelled - Sync status unknown. Please ensure that HearThis and HearThisAndroid versions are compatible. + Sync had an error. Please try again. Choose a project Choose a book Choose a chapter From 00f8cb08ba9e55905e70bd20a1476320c5595731 Mon Sep 17 00:00:00 2001 From: Wade Mergenthal Date: Wed, 1 Oct 2025 11:42:48 -0400 Subject: [PATCH 6/8] Safety commit: watchdog works, debug info shows sync activities 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. --- .../main/java/org/sil/hearthis/SyncActivity.java | 13 +++++-------- app/src/main/java/org/sil/hearthis/Watchdog.java | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/sil/hearthis/SyncActivity.java b/app/src/main/java/org/sil/hearthis/SyncActivity.java index 61bc90e..5d3673f 100644 --- a/app/src/main/java/org/sil/hearthis/SyncActivity.java +++ b/app/src/main/java/org/sil/hearthis/SyncActivity.java @@ -298,7 +298,6 @@ public void onNotification(String message) { //watchdog.pet(); watchdog.shutdown(); setProgress(getString(R.string.sync_success)); - Log.d("Sync", "onNotification, sync_success"); // WM, TEMPORARY break; case "sync_interrupted": Log.d("Sync", "onNotification.interrupted, shut down watchdog"); // WM, temporary @@ -306,7 +305,6 @@ public void onNotification(String message) { watchdog.shutdown(); // Sync was interrupted or cancelled. setProgress(getString(R.string.sync_interrupted)); - Log.d("Sync", "onNotification, sync_interrupted"); // WM, TEMPORARY break; case "sync_error": // Internal HTA error or incompatible versions of HT and HTA. @@ -314,7 +312,6 @@ public void onNotification(String message) { //watchdog.pet(); watchdog.shutdown(); setProgress(getString(R.string.sync_error)); - Log.d("Sync", "onNotification, sync_error"); // WM, TEMPORARY break; //case "sync_unknown": // // Likely caused by incompatible versions of HT and HTA. @@ -327,7 +324,7 @@ public void onNotification(String message) { //watchdog.pet(); watchdog.shutdown(); setProgress(getString(R.string.sync_error)); - Log.d("Sync", "onNotification, bad status: " + message); + Log.d("Sync", "onNotification.default, bad status: " + message); break; } runOnUiThread(new Runnable() { @@ -351,7 +348,7 @@ public void run() { @Override public void receivingFile(final String name) { - Log.d("Sync", "receivingFile, pet watchdog"); // WM, temporary + Log.d("Sync", " receivingFile, pet watchdog"); // WM, temporary watchdog.pet(); // To prevent excess flicker and wasting compute time on progress reports, @@ -360,18 +357,18 @@ public void receivingFile(final String name) { return; lastProgress = new Date(); setProgress("receiving " + name); - Log.d("Sync", "receivingFile: " + name); + Log.d("Sync", "receivingFile: " + name); // WM, temporary } @Override public void sendingFile(final String name) { - Log.d("Sync", "sendingFile, pet watchdog"); // WM, temporary + Log.d("Sync", " sendingFile, pet watchdog"); // WM, temporary watchdog.pet(); if (new Date().getTime() - lastProgress.getTime() < 1000) return; lastProgress = new Date(); setProgress("sending " + name); - Log.d("Sync", "sendingFile: " + name); + Log.d("Sync", "sendingFile: " + name); // WM, temporary } } diff --git a/app/src/main/java/org/sil/hearthis/Watchdog.java b/app/src/main/java/org/sil/hearthis/Watchdog.java index 90eeada..1165e55 100644 --- a/app/src/main/java/org/sil/hearthis/Watchdog.java +++ b/app/src/main/java/org/sil/hearthis/Watchdog.java @@ -34,8 +34,8 @@ public class Watchdog { private final TimeUnit unit; public Watchdog(long timeout, TimeUnit unit, Runnable onTimeout) { - Log.d("Sync", "Watchdog, constructor begin, timeout = " + timeout); // WM, temporary - Log.d("Sync", " unit = " + unit); // WM, temporary + Log.d("Sync", "Watchdog, constructor begin, timeout = " + timeout); // WM, temporary + Log.d("Sync", " unit = " + unit); // WM, temporary this.timeout = timeout; this.unit = unit; this.onTimeout = onTimeout; From a875cf3dcb6bd76054f78bc5f7f25c8bc2564041 Mon Sep 17 00:00:00 2001 From: Wade Mergenthal Date: Wed, 1 Oct 2025 18:10:33 -0400 Subject: [PATCH 7/8] watchdog: start and stop in better ways 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. --- .../java/org/sil/hearthis/SyncActivity.java | 45 ++++++++----------- .../main/java/org/sil/hearthis/Watchdog.java | 41 +++++++---------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/org/sil/hearthis/SyncActivity.java b/app/src/main/java/org/sil/hearthis/SyncActivity.java index 5d3673f..c97aa17 100644 --- a/app/src/main/java/org/sil/hearthis/SyncActivity.java +++ b/app/src/main/java/org/sil/hearthis/SyncActivity.java @@ -1,7 +1,6 @@ package org.sil.hearthis; import static org.sil.hearthis.AcceptNotificationHandler.notificationListeners; -//import org.sil.hearthis.Watchdog; import android.Manifest; import android.annotation.SuppressLint; @@ -57,14 +56,9 @@ public class SyncActivity extends AppCompatActivity implements AcceptNotificatio private static final int WATCHDOG_TIMEOUT_SECONDS = 10; boolean scanning = false; TextView progressView; - - Watchdog watchdog = new Watchdog(WATCHDOG_TIMEOUT_SECONDS, TimeUnit.SECONDS, () -> { - Log.d("Sync", "Watchdog, TIMED OUT, setting Error"); - setProgress(getString(R.string.sync_error)); - }); - private BarcodeDetector barcodeDetector; private CameraSource cameraSource; + private Watchdog watchdog; @Override protected void onCreate(Bundle savedInstanceState) { @@ -178,6 +172,14 @@ public void run() { Log.d("Sync", "SyncActivity.run, sending UDP packet"); // WM, TEMPORARY //throw new IOException("TEST HACK"); // WM, test only! socket.send(packet); // WM, comment out if preceding throw(), a hack, is present + + // Don't create and start the watchdog until we KNOW that we are doing a sync. + // At this point we have responded to the PC's sync offer and are indeed committed. + watchdog = new Watchdog(WATCHDOG_TIMEOUT_SECONDS, TimeUnit.SECONDS, () -> { + Log.d("Sync", "Watchdog, TIMED OUT, setting Error"); + setProgress(getString(R.string.sync_error)); + }); + Log.d("Sync", "SyncActivity.run, watchdog started, timeout = " + WATCHDOG_TIMEOUT_SECONDS + " secs"); } catch (IOException ioe) { // Note: this also catches UnknownHostException, a subclass of IOException for (AcceptNotificationHandler.NotificationListener listener : notificationListeners.toArray(new AcceptNotificationHandler.NotificationListener[notificationListeners.size()])) { @@ -289,40 +291,29 @@ public void onNotification(String message) { Log.d("Sync", "onNotification(" + message + "), calling removeNotificationListener()"); // WM, TEMPORARY AcceptNotificationHandler.removeNotificationListener(this); + // The watchdog timer prevents the Android app from getting stuck if the PC side + // is unable to complete a sync operation. Getting here means we got a notification + // from the PC. It should contain the final sync status, but even if it doesn't, the + // sync operation *is* complete and the watchdog should be turned off. + Log.d("Sync", "onNotification, got " + message + ", shutting down watchdog"); // WM, temporary + watchdog.shutdown(); + // HT-508: HearThis PC now includes sync status in its notification to the app. - // Handling status here in HearThisAndroid should prevent the app from getting - // into a bad state, besides informing the user about whether sync succeeded. + // We can now inform the user about whether sync succeeded. switch (message) { case "sync_success": - Log.d("Sync", "onNotification.success, shut down watchdog"); // WM, temporary - //watchdog.pet(); - watchdog.shutdown(); setProgress(getString(R.string.sync_success)); break; case "sync_interrupted": - Log.d("Sync", "onNotification.interrupted, shut down watchdog"); // WM, temporary - //watchdog.pet(); - watchdog.shutdown(); // Sync was interrupted or cancelled. setProgress(getString(R.string.sync_interrupted)); break; case "sync_error": // Internal HTA error or incompatible versions of HT and HTA. - Log.d("Sync", "onNotification.error, shut down watchdog"); // WM, temporary - //watchdog.pet(); - watchdog.shutdown(); setProgress(getString(R.string.sync_error)); break; - //case "sync_unknown": - // // Likely caused by incompatible versions of HT and HTA. - // setProgress(getString(R.string.sync_unknown)); - // Log.d("Sync", "onNotification, sync_unknown"); // WM, TEMPORARY - // break; default: - // Should never happen. Raise an error. - Log.d("Sync", "onNotification.default, shut down watchdog"); // WM, temporary - //watchdog.pet(); - watchdog.shutdown(); + // Not a sync status; should never happen. Raise an error. setProgress(getString(R.string.sync_error)); Log.d("Sync", "onNotification.default, bad status: " + message); break; diff --git a/app/src/main/java/org/sil/hearthis/Watchdog.java b/app/src/main/java/org/sil/hearthis/Watchdog.java index 1165e55..d3e12de 100644 --- a/app/src/main/java/org/sil/hearthis/Watchdog.java +++ b/app/src/main/java/org/sil/hearthis/Watchdog.java @@ -1,29 +1,18 @@ package org.sil.hearthis; -import android.util.Log; - -//import org.apache.http.HttpException; -//import org.apache.http.impl.DefaultConnectionReuseStrategy; -//import org.apache.http.impl.DefaultHttpResponseFactory; -//import org.apache.http.impl.DefaultHttpServerConnection; -//import org.apache.http.params.BasicHttpParams; -//import org.apache.http.protocol.BasicHttpContext; -//import org.apache.http.protocol.BasicHttpProcessor; -//import org.apache.http.protocol.HttpRequestHandlerRegistry; -//import org.apache.http.protocol.HttpService; -//import org.apache.http.protocol.ResponseConnControl; -//import org.apache.http.protocol.ResponseContent; -//import org.apache.http.protocol.ResponseDate; -//import org.apache.http.protocol.ResponseServer; - -//import java.io.IOException; -//import java.net.ServerSocket; -//import java.net.Socket; import java.util.concurrent.*; +import android.util.Log; /** - * This class implements a timeout for the Android side of a HearThis sync operation. + * This class implements a "watchdog" timer for the Android side of a HearThis sync operation. * + * Once instantiated and started, it counts down from its timeout value (passed in). The timer + * is NOT supposed to get all the way down to 0. If it does, a problematic condition has arisen + * somewhere and the 'onTimeout' code runs in an effort to mitigate the problem. + * Calling pet() restarts a full countdown. The timeout value should be chosen such that it is + * longer than any normal interval between calls to pet(). Thus in a correctly working system, + * pet() keeps getting called well before the timer ever finishes counting down to 0 from its + * initial timeout value, and the 'onTimeout' code never runs. */ public class Watchdog { @@ -34,20 +23,22 @@ public class Watchdog { private final TimeUnit unit; public Watchdog(long timeout, TimeUnit unit, Runnable onTimeout) { - Log.d("Sync", "Watchdog, constructor begin, timeout = " + timeout); // WM, temporary - Log.d("Sync", " unit = " + unit); // WM, temporary + //Log.d("Sync", "Watchdog, constructor, timeout = " + timeout); // WM, temporary + //Log.d("Sync", " unit = " + unit); // WM, temporary this.timeout = timeout; this.unit = unit; this.onTimeout = onTimeout; } - // Call this whenever input is received + // Subsystems of interest call this method to restart the timer countdown. Basically this + // means: "At the moment all is well. We'll try to call again before your next deadline. If + // we don't, send for help." public synchronized void pet() { if (watchdogTask != null && !watchdogTask.isDone()) { - Log.d("Sync", "Watchdog, pet, setting cancel false"); // WM, temporary + Log.d("Sync", "Watchdog, pet, not null and not done"); // WM, temporary watchdogTask.cancel(false); } - Log.d("Sync", "Watchdog, pet, calling scheduler.schedule()"); // WM, temporary + //Log.d("Sync", "Watchdog, pet, calling scheduler.schedule()"); // WM, temporary watchdogTask = scheduler.schedule(onTimeout, timeout, unit); } From fd787610ed31bdb609c29d621bf4426f65ff070c Mon Sep 17 00:00:00 2001 From: Wade Mergenthal Date: Wed, 1 Oct 2025 21:39:14 -0400 Subject: [PATCH 8/8] Ungray the Continue button upon watchdog timeout 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. --- .../java/org/sil/hearthis/AcceptNotificationHandler.java | 8 ++------ app/src/main/java/org/sil/hearthis/SyncActivity.java | 4 ++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java index ce45323..31625dc 100644 --- a/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java +++ b/app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java @@ -81,12 +81,8 @@ public void handle(HttpRequest request, HttpResponse response, HttpContext httpC } if (status == null) { - // Something went wrong. Make sure the user sees a non-success message. - //if (minHtaVersion != null) { - // status = "sync_unknown"; // error also: we got something but it wasn't "status" - //} else { - status = "sync_error"; - //} + // We got something but it wasn't "status". Make sure the user sees an error message. + status = "sync_error"; } Log.d("Sync", "handle, final from HT, status = " + status); // WM, TEMPORARY diff --git a/app/src/main/java/org/sil/hearthis/SyncActivity.java b/app/src/main/java/org/sil/hearthis/SyncActivity.java index c97aa17..b2de3d8 100644 --- a/app/src/main/java/org/sil/hearthis/SyncActivity.java +++ b/app/src/main/java/org/sil/hearthis/SyncActivity.java @@ -175,8 +175,12 @@ public void run() { // Don't create and start the watchdog until we KNOW that we are doing a sync. // At this point we have responded to the PC's sync offer and are indeed committed. + // NOTE: inside the braces is the mitigation code, running only if timeout occurs. watchdog = new Watchdog(WATCHDOG_TIMEOUT_SECONDS, TimeUnit.SECONDS, () -> { Log.d("Sync", "Watchdog, TIMED OUT, setting Error"); + for (AcceptNotificationHandler.NotificationListener listener: notificationListeners.toArray(new AcceptNotificationHandler.NotificationListener[notificationListeners.size()])) { + listener.onNotification("sync_error"); + } setProgress(getString(R.string.sync_error)); }); Log.d("Sync", "SyncActivity.run, watchdog started, timeout = " + WATCHDOG_TIMEOUT_SECONDS + " secs");