diff --git a/plugin.xml b/plugin.xml index 82ec955..12685c6 100644 --- a/plugin.xml +++ b/plugin.xml @@ -74,6 +74,7 @@ + @@ -92,4 +93,4 @@ - \ No newline at end of file + diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java index e56be1c..c107e3a 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpBase.java @@ -5,6 +5,7 @@ import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.IOException; +import java.io.InterruptedIOException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; @@ -18,8 +19,6 @@ import com.silkimen.http.HttpRequest.HttpRequestException; import com.silkimen.http.JsonUtils; import com.silkimen.http.TLSConfiguration; -import org.apache.cordova.CallbackContext; - import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -39,11 +38,11 @@ abstract class CordovaHttpBase implements Runnable { protected int timeout; protected boolean followRedirects; protected TLSConfiguration tlsConfiguration; - protected CallbackContext callbackContext; + protected CordovaObservableCallbackContext callbackContext; public CordovaHttpBase(String method, String url, String serializer, Object data, JSONObject headers, int timeout, boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, - CallbackContext callbackContext) { + CordovaObservableCallbackContext callbackContext) { this.method = method; this.url = url; @@ -58,7 +57,7 @@ abstract class CordovaHttpBase implements Runnable { } public CordovaHttpBase(String method, String url, JSONObject headers, int timeout, boolean followRedirects, - String responseType, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + String responseType, TLSConfiguration tlsConfiguration, CordovaObservableCallbackContext callbackContext) { this.method = method; this.url = url; @@ -74,8 +73,9 @@ abstract class CordovaHttpBase implements Runnable { public void run() { CordovaHttpResponse response = new CordovaHttpResponse(); + HttpRequest request = null; try { - HttpRequest request = this.createRequest(); + request = this.createRequest(); this.prepareRequest(request); this.sendBody(request); this.processResponse(request, response); @@ -94,10 +94,17 @@ abstract class CordovaHttpBase implements Runnable { response.setErrorMessage("Request timed out: " + e.getMessage()); Log.w(TAG, "Request timed out", e); } else { - response.setStatus(-1); - response.setErrorMessage("There was an error with the request: " + e.getCause().getMessage()); - Log.w(TAG, "Generic request error", e); + String cause = e.getCause().getMessage(); + if(e.getCause() instanceof InterruptedIOException && "thread interrupted".equals(cause.toLowerCase())){ + this.setAborted(request, response); + } else { + response.setStatus(-1); + response.setErrorMessage("There was an error with the request: " + cause); + Log.w(TAG, "Generic request error", e); + } } + } catch (InterruptedException ie) { + this.setAborted(request, response); } catch (Exception e) { response.setStatus(-1); response.setErrorMessage(e.getMessage()); @@ -202,4 +209,17 @@ abstract class CordovaHttpBase implements Runnable { response.setErrorMessage(HttpBodyDecoder.decodeBody(outputStream.toByteArray(), request.charset())); } } + + protected void setAborted(HttpRequest request, CordovaHttpResponse response) { + response.setStatus(-8); + response.setErrorMessage("Request was aborted"); + if(request != null){ + try{ + request.disconnect(); + } catch(Exception any){ + Log.w(TAG, "Failed to close aborted request", any); + } + } + Log.i(TAG, "Request was aborted"); + } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java index d89db82..975f5cc 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpDownload.java @@ -9,7 +9,6 @@ import javax.net.ssl.SSLSocketFactory; import com.silkimen.http.HttpRequest; import com.silkimen.http.TLSConfiguration; -import org.apache.cordova.CallbackContext; import org.apache.cordova.file.FileUtils; import org.json.JSONObject; @@ -17,7 +16,7 @@ class CordovaHttpDownload extends CordovaHttpBase { private String filePath; public CordovaHttpDownload(String url, JSONObject headers, String filePath, int timeout, boolean followRedirects, - TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + TLSConfiguration tlsConfiguration, CordovaObservableCallbackContext callbackContext) { super("GET", url, headers, timeout, followRedirects, "text", tlsConfiguration, callbackContext); this.filePath = filePath; diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java index 5f17e5d..6eacca5 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpOperation.java @@ -5,20 +5,19 @@ import javax.net.ssl.SSLSocketFactory; import com.silkimen.http.TLSConfiguration; -import org.apache.cordova.CallbackContext; import org.json.JSONObject; class CordovaHttpOperation extends CordovaHttpBase { public CordovaHttpOperation(String method, String url, String serializer, Object data, JSONObject headers, int timeout, boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, - CallbackContext callbackContext) { + CordovaObservableCallbackContext callbackContext) { super(method, url, serializer, data, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext); } public CordovaHttpOperation(String method, String url, JSONObject headers, int timeout, boolean followRedirects, - String responseType, TLSConfiguration tlsConfiguration, CallbackContext callbackContext) { + String responseType, TLSConfiguration tlsConfiguration, CordovaObservableCallbackContext callbackContext) { super(method, url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext); } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java index 297dba3..8e9f9e7 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpPlugin.java @@ -1,6 +1,10 @@ package com.silkimen.cordovahttp; import java.security.KeyStore; +import java.util.HashMap; +import java.util.Observable; +import java.util.Observer; +import java.util.concurrent.Future; import com.silkimen.http.TLSConfiguration; @@ -17,17 +21,22 @@ import android.util.Base64; import javax.net.ssl.TrustManagerFactory; -public class CordovaHttpPlugin extends CordovaPlugin { +public class CordovaHttpPlugin extends CordovaPlugin implements Observer { private static final String TAG = "Cordova-Plugin-HTTP"; private TLSConfiguration tlsConfiguration; + private HashMap> reqMap; + private final Object reqMapLock = new Object(); + @Override public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); this.tlsConfiguration = new TLSConfiguration(); + this.reqMap = new HashMap>(); + try { KeyStore store = KeyStore.getInstance("AndroidCAStore"); String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); @@ -73,6 +82,8 @@ public class CordovaHttpPlugin extends CordovaPlugin { return this.setServerTrustMode(args, callbackContext); } else if ("setClientAuthMode".equals(action)) { return this.setClientAuthMode(args, callbackContext); + } else if ("abort".equals(action)) { + return this.abort(args, callbackContext); } else { return false; } @@ -87,10 +98,14 @@ public class CordovaHttpPlugin extends CordovaPlugin { boolean followRedirect = args.getBoolean(3); String responseType = args.getString(4); - CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect, - responseType, this.tlsConfiguration, callbackContext); + Integer reqId = args.getInt(5); + CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId); - cordova.getThreadPool().execute(request); + CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, headers, timeout, followRedirect, + responseType, this.tlsConfiguration, observableCallbackContext); + + Future task = cordova.getThreadPool().submit(request); + this.addReq(reqId, task, observableCallbackContext); return true; } @@ -106,10 +121,14 @@ public class CordovaHttpPlugin extends CordovaPlugin { boolean followRedirect = args.getBoolean(5); String responseType = args.getString(6); - CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers, - timeout, followRedirect, responseType, this.tlsConfiguration, callbackContext); + Integer reqId = args.getInt(7); + CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId); - cordova.getThreadPool().execute(request); + CordovaHttpOperation request = new CordovaHttpOperation(method.toUpperCase(), url, serializer, data, headers, + timeout, followRedirect, responseType, this.tlsConfiguration, observableCallbackContext); + + Future task = cordova.getThreadPool().submit(request); + this.addReq(reqId, task, observableCallbackContext); return true; } @@ -123,10 +142,14 @@ public class CordovaHttpPlugin extends CordovaPlugin { boolean followRedirect = args.getBoolean(5); String responseType = args.getString(6); - CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, timeout, followRedirect, - responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), callbackContext); + Integer reqId = args.getInt(7); + CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId); - cordova.getThreadPool().execute(upload); + CordovaHttpUpload upload = new CordovaHttpUpload(url, headers, filePaths, uploadNames, timeout, followRedirect, + responseType, this.tlsConfiguration, this.cordova.getActivity().getApplicationContext(), observableCallbackContext); + + Future task = cordova.getThreadPool().submit(upload); + this.addReq(reqId, task, observableCallbackContext); return true; } @@ -138,10 +161,14 @@ public class CordovaHttpPlugin extends CordovaPlugin { int timeout = args.getInt(3) * 1000; boolean followRedirect = args.getBoolean(4); - CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, followRedirect, - this.tlsConfiguration, callbackContext); + Integer reqId = args.getInt(5); + CordovaObservableCallbackContext observableCallbackContext = new CordovaObservableCallbackContext(callbackContext, reqId); - cordova.getThreadPool().execute(download); + CordovaHttpDownload download = new CordovaHttpDownload(url, headers, filePath, timeout, followRedirect, + this.tlsConfiguration, observableCallbackContext); + + Future task = cordova.getThreadPool().submit(download); + this.addReq(reqId, task, observableCallbackContext); return true; } @@ -166,4 +193,49 @@ public class CordovaHttpPlugin extends CordovaPlugin { return true; } + + private boolean abort(final JSONArray args, final CallbackContext callbackContext) throws JSONException { + + int reqId = args.getInt(0); + boolean result = false; + // NOTE no synchronized (reqMapLock), since even if the req was already removed from reqMap, + // the worst that would happen calling task.cancel(true) is a result of false + // (i.e. same result as locking & not finding the req in reqMap) + Future task = this.reqMap.get(reqId); + if (task != null && !task.isDone()) { + result = task.cancel(true); + } + callbackContext.success(new JSONObject().put("aborted", result)); + + return true; + } + + private void addReq(final Integer reqId, final Future task, final CordovaObservableCallbackContext observableCallbackContext) { + synchronized (reqMapLock) { + // NOTE there is a small chance that the task may already have tried to remove itself before + // done-status was set (within the request run-thread) + // to prevent that, the synchronized()-lock would need to be set around starting the + // request and adding the entry to reqMap (which seems overkill given that is seems very unlikely) + if(!task.isDone()){ + observableCallbackContext.setObserver(this); + this.reqMap.put(reqId, task); + } + } + } + + private void removeReq(final Integer reqId) { + synchronized (reqMapLock) { + this.reqMap.remove(reqId); + } + } + + @Override + public void update(Observable o, Object arg) { + synchronized (reqMapLock) { + CordovaObservableCallbackContext c = (CordovaObservableCallbackContext) arg; + if (c.getCallbackContext().isFinished()) { + removeReq(c.getRequestId()); + } + } + } } diff --git a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java index dcbcd06..a62a98a 100644 --- a/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java +++ b/src/android/com/silkimen/cordovahttp/CordovaHttpUpload.java @@ -17,7 +17,6 @@ import java.net.URI; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; -import org.apache.cordova.CallbackContext; import org.json.JSONArray; import org.json.JSONObject; @@ -28,7 +27,7 @@ class CordovaHttpUpload extends CordovaHttpBase { public CordovaHttpUpload(String url, JSONObject headers, JSONArray filePaths, JSONArray uploadNames, int timeout, boolean followRedirects, String responseType, TLSConfiguration tlsConfiguration, - Context applicationContext, CallbackContext callbackContext) { + Context applicationContext, CordovaObservableCallbackContext callbackContext) { super("POST", url, headers, timeout, followRedirects, responseType, tlsConfiguration, callbackContext); this.filePaths = filePaths; diff --git a/src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java b/src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java new file mode 100644 index 0000000..cff0583 --- /dev/null +++ b/src/android/com/silkimen/cordovahttp/CordovaObservableCallbackContext.java @@ -0,0 +1,58 @@ +package com.silkimen.cordovahttp; + +import org.apache.cordova.CallbackContext; +import org.json.JSONObject; + +import java.util.Observer; + +public class CordovaObservableCallbackContext { + + private CallbackContext callbackContext; + private Integer requestId; + private Observer observer; + + public CordovaObservableCallbackContext(CallbackContext callbackContext, Integer requestId) { + this.callbackContext = callbackContext; + this.requestId = requestId; + } + + public void success(JSONObject message) { + this.callbackContext.success(message); + this.notifyObserver(); + } + + public void error(JSONObject message) { + this.callbackContext.error(message); + this.notifyObserver(); + } + + public Integer getRequestId() { + return this.requestId; + } + + public CallbackContext getCallbackContext() { + return callbackContext; + } + + public Observer getObserver() { + return observer; + } + + protected void notifyObserver() { + if(this.observer != null){ + this.observer.update(null, this); + } + } + + /** + * Set an observer that is notified, when {@link #success(JSONObject)} + * or {@link #error(JSONObject)} are called. + * + * NOTE the observer is notified with + *
observer.update(null, cordovaObservableCallbackContext)
+ * @param observer + */ + public void setObserver(Observer observer) { + this.observer = observer; + } +}