Skip to content
Open
36 changes: 34 additions & 2 deletions app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -32,9 +33,40 @@ 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.

// 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.
//
// NOTE: like several things in HearThisAndroid, HttpRequest is deprecated. It will be
// replaced with something more appropriate, hopefully soon.

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";
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
}

}

for (NotificationListener listener: notificationListeners.toArray(new NotificationListener[notificationListeners.size()])) {
listener.onNotification("");
listener.onNotification(status);
}
response.setEntity(new StringEntity("success"));
response.setEntity(new StringEntity(status));
}
}
72 changes: 40 additions & 32 deletions app/src/main/java/org/sil/hearthis/SyncActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
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;
import androidx.core.app.ActivityCompat;

import android.os.Handler;
import android.os.Looper;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuItem;
Expand All @@ -32,6 +33,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,
Expand Down Expand Up @@ -125,6 +128,8 @@ public void release() {
// Toast.makeText(getApplicationContext(), "To prevent memory leaks barcode scanner has been stopped", Toast.LENGTH_SHORT).show();
}

// 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<Barcode> detections) {
final SparseArray<Barcode> barcodes = detections.getDetectedItems();
Expand All @@ -145,15 +150,32 @@ 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();
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();
}
handler.post(() -> {
// Background work done, no associated foreground work needed.
});
});
cameraSource.stop();
cameraSource.release();
cameraSource = null;
}
});

}
}
}
Expand Down Expand Up @@ -216,11 +238,8 @@ private String getOurIpAddress() {
if (inetAddress.isSiteLocalAddress()) {
return inetAddress.getHostAddress();
}

}

}

} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Expand Down Expand Up @@ -248,7 +267,19 @@ public boolean onOptionsItemSelected(MenuItem item) {
@Override
public void onNotification(String 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")) {
setProgress(getString(R.string.sync_success));
} 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);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
Expand Down Expand Up @@ -285,27 +316,4 @@ public void sendingFile(final String name) {
lastProgress = new Date();
setProgress("sending " + 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<Void, Void, Void> {

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;
}
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<string name="continue_button_caption">Continue</string>
<string name="title_activity_choose_book">ChooseBookActivity</string>
<string name="sync_success">Sync completed successfully!</string>
<string name="sync_interrupted">Sync was interrupted or cancelled</string>
<string name="choose_project">Choose a project</string>
<string name="choose_book">Choose a book</string>
<string name="choose_chapter">Choose a chapter</string>
Expand Down