Skip to content
Open
5 changes: 4 additions & 1 deletion app/src/main/java/org/sil/hearthis/AcceptFileHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
60 changes: 58 additions & 2 deletions app/src/main/java/org/sil/hearthis/AcceptNotificationHandler.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
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;
import org.apache.http.entity.StringEntity;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;

/**
* Created by Thomson on 1/18/2016.
*/
public class AcceptNotificationHandler implements HttpRequestHandler {

private static String minHtaVersion = null;

public interface NotificationListener {
void onNotification(String message);
}
Expand All @@ -32,9 +37,60 @@ 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. 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();
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) {
// 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
for (NotificationListener listener: notificationListeners.toArray(new NotificationListener[notificationListeners.size()])) {
listener.onNotification("");
Log.d("Sync", "handle, calling listener.onNotification(" + status + ")"); // WM, TEMPORARY
listener.onNotification(status);
}
response.setEntity(new StringEntity("success"));
Log.d("Sync", "handle, putting status in response (I think)"); // WM, TEMPORARY
response.setEntity(new StringEntity(status));
}
}
5 changes: 4 additions & 1 deletion app/src/main/java/org/sil/hearthis/RequestFileHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down
126 changes: 92 additions & 34 deletions app/src/main/java/org/sil/hearthis/SyncActivity.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package org.sil.hearthis;

import static org.sil.hearthis.AcceptNotificationHandler.notificationListeners;

import android.Manifest;
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.Log;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuItem;
Expand All @@ -23,15 +27,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,
Expand All @@ -44,11 +53,12 @@ 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;

private BarcodeDetector barcodeDetector;
private CameraSource cameraSource;
private Watchdog watchdog;

@Override
protected void onCreate(Bundle savedInstanceState) {
Expand Down Expand Up @@ -125,6 +135,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 +157,50 @@ 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", "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);
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.
// 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");
} 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.
});
});
cameraSource.stop();
cameraSource.release();
cameraSource = null;
}
});

}
}
}
Expand All @@ -176,6 +223,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;
}
Expand Down Expand Up @@ -216,11 +264,8 @@ private String getOurIpAddress() {
if (inetAddress.isSiteLocalAddress()) {
return inetAddress.getHostAddress();
}

}

}

} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Expand All @@ -247,8 +292,36 @@ public boolean onOptionsItemSelected(MenuItem item) {

@Override
public void onNotification(String message) {
Log.d("Sync", "onNotification(" + message + "), calling removeNotificationListener()"); // WM, TEMPORARY
AcceptNotificationHandler.removeNotificationListener(this);
setProgress(getString(R.string.sync_success));

// 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.
// We can now inform the user about whether sync succeeded.
switch (message) {
case "sync_success":
setProgress(getString(R.string.sync_success));
break;
case "sync_interrupted":
// 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.
setProgress(getString(R.string.sync_error));
break;
default:
// 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;
}
runOnUiThread(new Runnable() {
@Override
public void run() {
Expand All @@ -270,42 +343,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); // WM, temporary
}

@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);
}

// 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;
}
Log.d("Sync", "sendingFile: " + name); // WM, temporary
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/org/sil/hearthis/SyncServer.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Loading