Updated Branches:
  refs/heads/master 5289d569b -> 05bc1865a

Change FileTransfer to use the new plugin signature.

Fixes slow abort(): https://issues.apache.org/jira/browse/CB-1516
Fixes abort() race condition: https://issues.apache.org/jira/browse/CB-1532


Project: http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/commit/05bc1865
Tree: 
http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/tree/05bc1865
Diff: 
http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/diff/05bc1865

Branch: refs/heads/master
Commit: 05bc1865a63a09a2fde92adeb1ffc59884ef4dc3
Parents: 6e6e027
Author: Andrew Grieve <agri...@chromium.org>
Authored: Tue Sep 25 13:25:06 2012 -0400
Committer: Andrew Grieve <agri...@chromium.org>
Committed: Tue Oct 2 10:14:52 2012 -0400

----------------------------------------------------------------------
 framework/src/org/apache/cordova/FileTransfer.java |  885 ++++++++-------
 1 files changed, 481 insertions(+), 404 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-cordova-android/blob/05bc1865/framework/src/org/apache/cordova/FileTransfer.java
----------------------------------------------------------------------
diff --git a/framework/src/org/apache/cordova/FileTransfer.java 
b/framework/src/org/apache/cordova/FileTransfer.java
index deee291..be3a11f 100644
--- a/framework/src/org/apache/cordova/FileTransfer.java
+++ b/framework/src/org/apache/cordova/FileTransfer.java
@@ -18,6 +18,7 @@
 */
 package org.apache.cordova;
 
+import java.io.Closeable;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.File;
@@ -27,13 +28,14 @@ import java.io.FileOutputStream;
 import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLDecoder;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
-import java.util.HashSet;
+import java.util.HashMap;
 import java.util.Iterator;
 
 import javax.net.ssl.HostnameVerifier;
@@ -68,15 +70,30 @@ public class FileTransfer extends CordovaPlugin {
     public static int CONNECTION_ERR = 3;
     public static int ABORTED_ERR = 4;
 
-    private static HashSet<String> abortTriggered = new HashSet<String>();
+    private static HashMap<String, RequestContext> activeRequests = new 
HashMap<String, RequestContext>();
+    private static final int MAX_BUFFER_SIZE = 16 * 1024;
 
-    private SSLSocketFactory defaultSSLSocketFactory = null;
-    private HostnameVerifier defaultHostnameVerifier = null;
 
-    private static final class AbortException extends Exception {
-        private static final long serialVersionUID = 1L;
-        public AbortException(String str) {
-            super(str);
+    private static SSLSocketFactory defaultSSLSocketFactory = null;
+
+    private static final class RequestContext {
+        String source;
+        String target;
+        CallbackContext callbackContext;
+        InputStream currentInputStream;
+        OutputStream currentOutputStream;
+        boolean aborted;
+        RequestContext(String source, String target, CallbackContext 
callbackContext) {
+            this.source = source;
+            this.target = target;
+            this.callbackContext = callbackContext;
+        }
+        void sendPluginResult(PluginResult pluginResult) {
+            synchronized (this) {
+                if (!aborted) {
+                    callbackContext.sendPluginResult(pluginResult);
+                }
+            }
         }
     }
 
@@ -113,21 +130,11 @@ public class FileTransfer extends CordovaPlugin {
         }
     }
     
-    /* (non-Javadoc)
-    * @see org.apache.cordova.api.Plugin#execute(java.lang.String, 
org.json.JSONArray, java.lang.String)
-    */
     @Override
     public boolean execute(String action, JSONArray args, final 
CallbackContext callbackContext) throws JSONException {
         if (action.equals("upload") || action.equals("download")) {
-            String source = null;
-            String target = null;
-            try {
-                source = args.getString(0);
-                target = args.getString(1);
-            } catch (JSONException e) {
-                Log.d(LOG_TAG, "Missing source or target");
-                return new PluginResult(PluginResult.Status.JSON_EXCEPTION, 
"Missing source or target");
-            }
+            String source = args.getString(0);
+            String target = args.getString(1);
 
             if (action.equals("upload")) {
                 upload(URLDecoder.decode(source), target, args, 
callbackContext);
@@ -157,273 +164,309 @@ public class FileTransfer extends CordovaPlugin {
      * args[5] params        key:value pairs of user-defined parameters
      * @return FileUploadResult containing result of upload request
      */
-    private PluginResult upload(String source, String target, JSONArray args, 
CallbackContext callbackContext) {
+    private void upload(final String source, final String target, JSONArray 
args, CallbackContext callbackContext) throws JSONException {
         Log.d(LOG_TAG, "upload " + source + " to " +  target);
 
-        HttpURLConnection conn = null;
+        // Setup the options
+        final String fileKey = getArgument(args, 2, "file");
+        final String fileName = getArgument(args, 3, "image.jpg");
+        final String mimeType = getArgument(args, 4, "image/jpeg");
+        final JSONObject params = args.optJSONObject(5) == null ? new 
JSONObject() : args.optJSONObject(5);
+        final boolean trustEveryone = args.optBoolean(6);
+        // Always use chunked mode unless set to false as per API
+        final boolean chunkedMode = args.optBoolean(7) || args.isNull(7);
+        // Look for headers on the params map for backwards compatibility with 
older Cordova versions.
+        final JSONObject headers = args.optJSONObject(8) == null ? 
params.optJSONObject("headers") : args.optJSONObject(8);
+        final String objectId = args.getString(9);
+
+        Log.d(LOG_TAG, "fileKey: " + fileKey);
+        Log.d(LOG_TAG, "fileName: " + fileName);
+        Log.d(LOG_TAG, "mimeType: " + mimeType);
+        Log.d(LOG_TAG, "params: " + params);
+        Log.d(LOG_TAG, "trustEveryone: " + trustEveryone);
+        Log.d(LOG_TAG, "chunkedMode: " + chunkedMode);
+        Log.d(LOG_TAG, "headers: " + headers);
+        Log.d(LOG_TAG, "objectId: " + objectId);
+        
+        final URL url;
         try {
-            // Setup the options
-            String fileKey = getArgument(args, 2, "file");
-            String fileName = getArgument(args, 3, "image.jpg");
-            String mimeType = getArgument(args, 4, "image/jpeg");
-            JSONObject params = args.optJSONObject(5);
-            if (params == null) params = new JSONObject();
-            boolean trustEveryone = args.optBoolean(6);
-            boolean chunkedMode = args.optBoolean(7) || args.isNull(7); 
//Always use chunked mode unless set to false as per API
-            JSONObject headers = args.optJSONObject(8);
-            // Look for headers on the params map for backwards compatibility 
with older Cordova versions.
-            if (headers == null && params != null) {
-                headers = params.optJSONObject("headers");
-            }
-            String objectId = args.getString(9);
-
-            Log.d(LOG_TAG, "fileKey: " + fileKey);
-            Log.d(LOG_TAG, "fileName: " + fileName);
-            Log.d(LOG_TAG, "mimeType: " + mimeType);
-            Log.d(LOG_TAG, "params: " + params);
-            Log.d(LOG_TAG, "trustEveryone: " + trustEveryone);
-            Log.d(LOG_TAG, "chunkedMode: " + chunkedMode);
-            Log.d(LOG_TAG, "headers: " + headers);
-            Log.d(LOG_TAG, "objectId: " + objectId);
-
-            // Create return object
-            FileUploadResult result = new FileUploadResult();
-            FileProgressResult progress = new FileProgressResult();
-
-            // Get a input stream of the file on the phone
-            InputStream inputStream = getPathFromUri(source);
-
-            DataOutputStream dos = null;
-
-            int bytesRead, bytesAvailable, bufferSize;
-            long totalBytes;
-            byte[] buffer;
-            int maxBufferSize = 8096;
-
-            //------------------ CLIENT REQUEST
-            // open a URL connection to the server
-            URL url = new URL(target);
-            boolean useHttps = url.getProtocol().toLowerCase().equals("https");
-            // Open a HTTP connection to the URL based on protocol
-            if (useHttps) {
-                // Using standard HTTPS connection. Will not allow self signed 
certificate
-                if (!trustEveryone) {
-                    conn = (HttpsURLConnection) url.openConnection();
-                }
-                // Use our HTTPS connection that blindly trusts everyone.
-                // This should only be used in debug environments
-                else {
-                    // Setup the HTTPS connection class to trust everyone
-                    trustAllHosts();
-                    HttpsURLConnection https = (HttpsURLConnection) 
url.openConnection();
-                    // Save the current hostnameVerifier
-                    defaultHostnameVerifier = https.getHostnameVerifier();
-                    // Setup the connection not to verify hostnames
-                    https.setHostnameVerifier(DO_NOT_VERIFY);
-                    conn = https;
+            url = new URL(target);
+        } catch (MalformedURLException e) {
+            JSONObject error = createFileTransferError(INVALID_URL_ERR, 
source, target, 0);
+            Log.e(LOG_TAG, error.toString(), e);
+            callbackContext.sendPluginResult(new 
PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+            return;
+        }
+        final boolean useHttps = 
url.getProtocol().toLowerCase().equals("https");
+
+        final RequestContext context = new RequestContext(source, target, 
callbackContext);
+        synchronized (activeRequests) {
+            activeRequests.put(objectId, context);
+        }
+        
+        cordova.getThreadPool().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (context.aborted) {
+                    return;
                 }
-            }
-            // Return a standard HTTP connection
-            else {
-                conn = (HttpURLConnection) url.openConnection();
-            }
+                HttpURLConnection conn = null;
+                HostnameVerifier defaultHostnameVerifier = null;
+                
+                try {
+                    // Create return object
+                    FileUploadResult result = new FileUploadResult();
+                    FileProgressResult progress = new FileProgressResult();
+
+                    //------------------ CLIENT REQUEST
+                    // Open a HTTP connection to the URL based on protocol
+                    if (useHttps) {
+                        // Using standard HTTPS connection. Will not allow 
self signed certificate
+                        if (!trustEveryone) {
+                            conn = (HttpsURLConnection) url.openConnection();
+                        }
+                        // Use our HTTPS connection that blindly trusts 
everyone.
+                        // This should only be used in debug environments
+                        else {
+                            // Setup the HTTPS connection class to trust 
everyone
+                            trustAllHosts();
+                            HttpsURLConnection https = (HttpsURLConnection) 
url.openConnection();
+                            // Save the current hostnameVerifier
+                            defaultHostnameVerifier = 
https.getHostnameVerifier();
+                            // Setup the connection not to verify hostnames
+                            https.setHostnameVerifier(DO_NOT_VERIFY);
+                            conn = https;
+                        }
+                    }
+                    // Return a standard HTTP connection
+                    else {
+                        conn = (HttpURLConnection) url.openConnection();
+                    }
 
-            // Allow Inputs
-            conn.setDoInput(true);
+                    // Allow Inputs
+                    conn.setDoInput(true);
 
-            // Allow Outputs
-            conn.setDoOutput(true);
+                    // Allow Outputs
+                    conn.setDoOutput(true);
 
-            // Don't use a cached copy.
-            conn.setUseCaches(false);
+                    // Don't use a cached copy.
+                    conn.setUseCaches(false);
 
-            // Use a post method.
-            conn.setRequestMethod("POST");
-            conn.setRequestProperty("Connection", "Keep-Alive");
-            conn.setRequestProperty("Content-Type", 
"multipart/form-data;boundary=" + BOUNDARY);
+                    // Use a post method.
+                    conn.setRequestMethod("POST");
+                    conn.setRequestProperty("Connection", "Keep-Alive");
+                    conn.setRequestProperty("Content-Type", 
"multipart/form-data;boundary=" + BOUNDARY);
 
-            // Set the cookies on the response
-            String cookie = CookieManager.getInstance().getCookie(target);
-            if (cookie != null) {
-                conn.setRequestProperty("Cookie", cookie);
-            }
+                    // Set the cookies on the response
+                    String cookie = 
CookieManager.getInstance().getCookie(target);
+                    if (cookie != null) {
+                        conn.setRequestProperty("Cookie", cookie);
+                    }
 
-            // Handle the other headers
-            if (headers != null) {
-                try {
-                    for (Iterator iter = headers.keys(); iter.hasNext(); ) {
-                        String headerKey = iter.next().toString();
-                        JSONArray headerValues = 
headers.optJSONArray(headerKey);
-                        if (headerValues == null) {
-                            headerValues = new JSONArray();
-                            headerValues.put(headers.getString(headerKey));
-                        }
-                        conn.setRequestProperty(headerKey, 
headerValues.getString(0));
-                        for (int i = 1; i < headerValues.length(); ++i) {
-                            conn.addRequestProperty(headerKey, 
headerValues.getString(i));
+                    // Handle the other headers
+                    if (headers != null) {
+                        try {
+                            for (Iterator iter = headers.keys(); 
iter.hasNext(); ) {
+                                String headerKey = iter.next().toString();
+                                JSONArray headerValues = 
headers.optJSONArray(headerKey);
+                                if (headerValues == null) {
+                                    headerValues = new JSONArray();
+                                    
headerValues.put(headers.getString(headerKey));
+                                }
+                                conn.setRequestProperty(headerKey, 
headerValues.getString(0));
+                                for (int i = 1; i < headerValues.length(); 
++i) {
+                                    conn.addRequestProperty(headerKey, 
headerValues.getString(i));
+                                }
+                            }
+                        } catch (JSONException e1) {
+                          // No headers to be manipulated!
                         }
                     }
-                } catch (JSONException e1) {
-                  // No headers to be manipulated!
-                }
-            }
 
-            /*
-                * Store the non-file portions of the multipart data as a 
string, so that we can add it
-                * to the contentSize, since it is part of the body of the HTTP 
request.
-                */
-            String extraParams = "";
-            try {
-                for (Iterator iter = params.keys(); iter.hasNext();) {
-                    Object key = iter.next();
-                    if(!String.valueOf(key).equals("headers"))
-                    {
-                      extraParams += LINE_START + BOUNDARY + LINE_END;
-                      extraParams += "Content-Disposition: form-data; name=\"" 
+  key.toString() + "\";";
-                      extraParams += LINE_END + LINE_END;
-                      extraParams += params.getString(key.toString());
-                      extraParams += LINE_END;
+                    /*
+                        * Store the non-file portions of the multipart data as 
a string, so that we can add it
+                        * to the contentSize, since it is part of the body of 
the HTTP request.
+                        */
+                    String extraParams = "";
+                    try {
+                        for (Iterator iter = params.keys(); iter.hasNext();) {
+                            Object key = iter.next();
+                            if(!String.valueOf(key).equals("headers"))
+                            {
+                              extraParams += LINE_START + BOUNDARY + LINE_END;
+                              extraParams += "Content-Disposition: form-data; 
name=\"" +  key.toString() + "\";";
+                              extraParams += LINE_END + LINE_END;
+                              extraParams += params.getString(key.toString());
+                              extraParams += LINE_END;
+                            }
+                        }
+                    } catch (JSONException e) {
+                        Log.e(LOG_TAG, e.getMessage(), e);
                     }
-                }
-            } catch (JSONException e) {
-                Log.e(LOG_TAG, e.getMessage(), e);
-            }
-
-            extraParams += LINE_START + BOUNDARY + LINE_END;
-            extraParams += "Content-Disposition: form-data; name=\"" + fileKey 
+ "\";" + " filename=\"";
-            byte[] extraBytes = extraParams.getBytes("UTF-8");
-
-            String midParams = "\"" + LINE_END + "Content-Type: " + mimeType + 
LINE_END + LINE_END;
-            String tailParams = LINE_END + LINE_START + BOUNDARY + LINE_START 
+ LINE_END;
-            byte[] fileNameBytes = fileName.getBytes("UTF-8");
-
-            int stringLength = extraBytes.length + midParams.length() + 
tailParams.length() + fileNameBytes.length;
-            Log.d(LOG_TAG, "String Length: " + stringLength);
-            int fixedLength = -1;
-            if (inputStream instanceof FileInputStream) {
-                fixedLength = (int) 
((FileInputStream)inputStream).getChannel().size() + stringLength;
-                progress.setLengthComputable(true);
-                progress.setTotal(fixedLength);
-            }
-            Log.d(LOG_TAG, "Content Length: " + fixedLength);
-            // setFixedLengthStreamingMode causes and OutOfMemoryException on 
pre-Froyo devices.
-            // http://code.google.com/p/android/issues/detail?id=3164
-            // It also causes OOM if HTTPS is used, even on newer devices.
-            chunkedMode = chunkedMode && (Build.VERSION.SDK_INT < 
Build.VERSION_CODES.FROYO || useHttps);
-            chunkedMode = chunkedMode || (fixedLength == -1);
-                       
-            if (chunkedMode) {
-                conn.setChunkedStreamingMode(maxBufferSize);
-                // Although setChunkedStreamingMode sets this header, setting 
it explicitly here works
-                // around an OutOfMemoryException when using https.
-                conn.setRequestProperty("Transfer-Encoding", "chunked");
-            } else {
-                conn.setFixedLengthStreamingMode(fixedLength);
-            }
 
-            dos = new DataOutputStream( conn.getOutputStream() );
-            //We don't want to change encoding, we just want this to write for 
all Unicode.
-            dos.write(extraBytes);
-            dos.write(fileNameBytes);
-            dos.writeBytes(midParams);
-
-            // create a buffer of maximum size
-            bytesAvailable = inputStream.available();
-            bufferSize = Math.min(bytesAvailable, maxBufferSize);
-            buffer = new byte[bufferSize];
-
-            // read file and write it into form...
-            bytesRead = inputStream.read(buffer, 0, bufferSize);
-            totalBytes = 0;
-
-            long prevBytesRead = 0;
-            while (bytesRead > 0) {
-                totalBytes += bytesRead;
-                result.setBytesSent(totalBytes);
-                dos.write(buffer, 0, bufferSize);
-                if (totalBytes > prevBytesRead + 102400) {
-                       prevBytesRead = totalBytes;
-                       Log.d(LOG_TAG, "Uploaded " + totalBytes + " of " + 
fixedLength + " bytes");
-                }
-                bytesAvailable = inputStream.available();
-                bufferSize = Math.min(bytesAvailable, maxBufferSize);
-                bytesRead = inputStream.read(buffer, 0, bufferSize);
-                if (objectId != null) {
-                    // Only send progress callbacks if the JS code sent us an 
object ID,
-                    // so we don't spam old versions with unrecognized 
callbacks.
-                    progress.setLoaded(totalBytes);
-                    PluginResult progressResult = new 
PluginResult(PluginResult.Status.OK, progress.toJSONObject());
-                    progressResult.setKeepCallback(true);
-                    callbackContext.sendPluginResult(progressResult);
-                }
-                synchronized (abortTriggered) {
-                    if (objectId != null && abortTriggered.contains(objectId)) 
{
-                        abortTriggered.remove(objectId);
-                        throw new AbortException("upload aborted");
+                    extraParams += LINE_START + BOUNDARY + LINE_END;
+                    extraParams += "Content-Disposition: form-data; name=\"" + 
fileKey + "\";" + " filename=\"";
+                    byte[] extraBytes = extraParams.getBytes("UTF-8");
+
+                    String midParams = "\"" + LINE_END + "Content-Type: " + 
mimeType + LINE_END + LINE_END;
+                    String tailParams = LINE_END + LINE_START + BOUNDARY + 
LINE_START + LINE_END;
+                    byte[] fileNameBytes = fileName.getBytes("UTF-8");
+
+                    
+                    // Get a input stream of the file on the phone
+                    InputStream sourceInputStream = getPathFromUri(source);
+                    
+                    int stringLength = extraBytes.length + midParams.length() 
+ tailParams.length() + fileNameBytes.length;
+                    Log.d(LOG_TAG, "String Length: " + stringLength);
+                    int fixedLength = -1;
+                    if (sourceInputStream instanceof FileInputStream) {
+                        fixedLength = (int) 
((FileInputStream)sourceInputStream).getChannel().size() + stringLength;
+                        progress.setLengthComputable(true);
+                        progress.setTotal(fixedLength);
+                    }
+                    Log.d(LOG_TAG, "Content Length: " + fixedLength);
+                    // setFixedLengthStreamingMode causes and 
OutOfMemoryException on pre-Froyo devices.
+                    // http://code.google.com/p/android/issues/detail?id=3164
+                    // It also causes OOM if HTTPS is used, even on newer 
devices.
+                    boolean useChunkedMode = chunkedMode && 
(Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO || useHttps);
+                    useChunkedMode = useChunkedMode || (fixedLength == -1);
+                            
+                    if (useChunkedMode) {
+                        conn.setChunkedStreamingMode(MAX_BUFFER_SIZE);
+                        // Although setChunkedStreamingMode sets this header, 
setting it explicitly here works
+                        // around an OutOfMemoryException when using https.
+                        conn.setRequestProperty("Transfer-Encoding", 
"chunked");
+                    } else {
+                        conn.setFixedLengthStreamingMode(fixedLength);
                     }
-                }
-            }
-
-            // send multipart form data necessary after file data...
-            dos.writeBytes(tailParams);
-
-            // close streams
-            inputStream.close();
-            dos.flush();
-            dos.close();
-
-            //------------------ read the SERVER RESPONSE
-            StringBuffer responseString = new StringBuffer("");
-            DataInputStream inStream = new 
DataInputStream(getInputStream(conn));
 
-            String line;
-            while (( line = inStream.readLine()) != null) {
-                responseString.append(line);
-            }
-            Log.d(LOG_TAG, "got response from server");
-            Log.d(LOG_TAG, responseString.toString());
+                    DataOutputStream dos = null;
+                    try {
+                        synchronized (context) {
+                            if (context.aborted) {
+                                throw new IOException("Request aborted");
+                            }
+                            dos = new DataOutputStream( conn.getOutputStream() 
);
+                            context.currentOutputStream = dos;
+                        }
+                        //We don't want to change encoding, we just want this 
to write for all Unicode.
+                        dos.write(extraBytes);
+                        dos.write(fileNameBytes);
+                        dos.writeBytes(midParams);
+    
+                        // create a buffer of maximum size
+                        int bytesAvailable = sourceInputStream.available();
+                        int bufferSize = Math.min(bytesAvailable, 
MAX_BUFFER_SIZE);
+                        byte[] buffer = new byte[bufferSize];
+    
+                        // read file and write it into form...
+                        int bytesRead = sourceInputStream.read(buffer, 0, 
bufferSize);
+                        long totalBytes = 0;
+    
+                        long prevBytesRead = 0;
+                        while (bytesRead > 0) {
+                            totalBytes += bytesRead;
+                            result.setBytesSent(totalBytes);
+                            dos.write(buffer, 0, bufferSize);
+                            if (totalBytes > prevBytesRead + 102400) {
+                                prevBytesRead = totalBytes;
+                                Log.d(LOG_TAG, "Uploaded " + totalBytes + " of 
" + fixedLength + " bytes");
+                            }
+                            bytesAvailable = sourceInputStream.available();
+                            bufferSize = Math.min(bytesAvailable, 
MAX_BUFFER_SIZE);
+                            bytesRead = sourceInputStream.read(buffer, 0, 
bufferSize);
+
+                            // Send a progress event.
+                            progress.setLoaded(totalBytes);
+                            PluginResult progressResult = new 
PluginResult(PluginResult.Status.OK, progress.toJSONObject());
+                            progressResult.setKeepCallback(true);
+                            context.sendPluginResult(progressResult);
+                        }
+    
+                        // send multipart form data necessary after file 
data...
+                        dos.writeBytes(tailParams);
+                        dos.flush();
+                    } finally {
+                        safeClose(sourceInputStream);
+                        safeClose(dos);
+                    }
+                    context.currentOutputStream = null;
+
+                    //------------------ read the SERVER RESPONSE
+                    StringBuffer responseString = new StringBuffer("");
+                    
+                    DataInputStream inStream = null;
+                    try {
+                        synchronized (context) {
+                            if (context.aborted) {
+                                throw new IOException("Request aborted");
+                            }
+                            inStream = new 
DataInputStream(getInputStream(conn));
+                            context.currentInputStream = inStream;
+                        }
+                        
+    
+                        String line;
+                        while (( line = inStream.readLine()) != null) {
+                            responseString.append(line);
+                        }
+                    } finally {
+                        safeClose(inStream);
+                    }
+                    
+                    Log.d(LOG_TAG, "got response from server");
+                    Log.d(LOG_TAG, responseString.toString());
+                    
+                    // send request and retrieve response
+                    result.setResponseCode(conn.getResponseCode());
+                    result.setResponse(responseString.toString());
+                    context.currentInputStream = null;
+                    synchronized (activeRequests) {
+                        activeRequests.remove(objectId);
+                    }
 
-            // send request and retrieve response
-            result.setResponseCode(conn.getResponseCode());
-            result.setResponse(responseString.toString());
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.OK, result.toJSONObject()));
+                } catch (FileNotFoundException e) {
+                    JSONObject error = 
createFileTransferError(FILE_NOT_FOUND_ERR, source, target, conn);
+                    Log.e(LOG_TAG, error.toString(), e);
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+                } catch (IOException e) {
+                    JSONObject error = createFileTransferError(CONNECTION_ERR, 
source, target, conn);
+                    Log.e(LOG_TAG, error.toString(), e);
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+                } catch (JSONException e) {
+                    Log.e(LOG_TAG, e.getMessage(), e);
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.JSON_EXCEPTION));
+                } catch (Throwable t) {
+                    // Shouldn't happen, but will
+                    JSONObject error = createFileTransferError(CONNECTION_ERR, 
source, target, conn);
+                    Log.e(LOG_TAG, error.toString(), t);
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+                } finally {
+                    synchronized (activeRequests) {
+                        activeRequests.remove(objectId);
+                    }
 
-            inStream.close();
+                    if (conn != null) {
+                        // Revert back to the proper verifier and socket 
factories
+                        if (trustEveryone && useHttps) {
+                            ((HttpsURLConnection) 
conn).setHostnameVerifier(defaultHostnameVerifier);
+                            
HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
+                        }
 
-            // Revert back to the proper verifier and socket factories
-            if (trustEveryone && 
url.getProtocol().toLowerCase().equals("https")) {
-                ((HttpsURLConnection) 
conn).setHostnameVerifier(defaultHostnameVerifier);
-                
HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
+                        conn.disconnect();
+                    }
+                }                
             }
+        });
+    }
 
-            Log.d(LOG_TAG, "****** About to return a result from upload");
-            return new PluginResult(PluginResult.Status.OK, 
result.toJSONObject());
-
-        } catch (FileNotFoundException e) {
-            JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, 
source, target, conn);
-            Log.e(LOG_TAG, error.toString(), e);
-            return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-        } catch (MalformedURLException e) {
-            JSONObject error = createFileTransferError(INVALID_URL_ERR, 
source, target, conn);
-            Log.e(LOG_TAG, error.toString(), e);
-            return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-        } catch (IOException e) {
-            JSONObject error = createFileTransferError(CONNECTION_ERR, source, 
target, conn);
-            Log.e(LOG_TAG, error.toString(), e);
-            return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-        } catch (JSONException e) {
-            Log.e(LOG_TAG, e.getMessage(), e);
-            return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
-        } catch (AbortException e) {
-            JSONObject error = createFileTransferError(ABORTED_ERR, source, 
target, conn);
-            return new PluginResult(PluginResult.Status.ERROR, error);
-        } catch (Throwable t) {
-            // Shouldn't happen, but will
-            JSONObject error = createFileTransferError(CONNECTION_ERR, source, 
target, conn);
-            Log.e(LOG_TAG, error.toString(), t);
-            return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-        } finally {
-            if (conn != null) {
-                conn.disconnect();
+    private static void safeClose(Closeable stream) {
+        if (stream != null) {
+            try {
+                stream.close();
+            } catch (IOException e) {
+                e.printStackTrace();
             }
         }
     }
@@ -469,7 +512,9 @@ public class FileTransfer extends CordovaPlugin {
         // Install the all-trusting trust manager
         try {
             // Backup the current SSL socket factory
-            defaultSSLSocketFactory = 
HttpsURLConnection.getDefaultSSLSocketFactory();
+            if (defaultSSLSocketFactory == null) {
+                defaultSSLSocketFactory = 
HttpsURLConnection.getDefaultSSLSocketFactory();
+            }
             // Install our all trusting manager
             SSLContext sc = SSLContext.getInstance("TLS");
             sc.init(null, trustAllCerts, new java.security.SecureRandom());
@@ -538,143 +583,166 @@ public class FileTransfer extends CordovaPlugin {
      *
      * @param source        URL of the server to receive the file
      * @param target           Full path of the file on the file system
-     * @return JSONObject      the downloaded file
      */
-    private PluginResult download(String source, String target, JSONArray 
args, CallbackContext callbackContext) {
+    private void download(final String source, final String target, JSONArray 
args, CallbackContext callbackContext) throws JSONException {
         Log.d(LOG_TAG, "download " + source + " to " +  target);
 
-        HttpURLConnection connection = null;
+        final boolean trustEveryone = args.optBoolean(2);
+        final String objectId = args.getString(3);
+
+        final URL url;
         try {
-            boolean trustEveryone = args.optBoolean(2);
-            String objectId = args.getString(3);
-            File file = getFileFromPath(target);
-
-            // create needed directories
-            file.getParentFile().mkdirs();
-
-            // connect to server
-            if (webView.isUrlWhiteListed(source))
-            {
-              URL url = new URL(source);
-              boolean useHttps = 
url.getProtocol().toLowerCase().equals("https");
-              // Open a HTTP connection to the URL based on protocol
-              if (useHttps) {
-                  // Using standard HTTPS connection. Will not allow self 
signed certificate
-                  if (!trustEveryone) {
-                         connection = (HttpsURLConnection) 
url.openConnection();
-                  }
-                  // Use our HTTPS connection that blindly trusts everyone.
-                  // This should only be used in debug environments
-                  else {
-                      // Setup the HTTPS connection class to trust everyone
-                      trustAllHosts();
-                      HttpsURLConnection https = (HttpsURLConnection) 
url.openConnection();
-                      // Save the current hostnameVerifier
-                      defaultHostnameVerifier = https.getHostnameVerifier();
-                      // Setup the connection not to verify hostnames
-                      https.setHostnameVerifier(DO_NOT_VERIFY);
-                      connection = https;
-                  }
-              }
-              // Return a standard HTTP connection
-              else {
-                 connection = (HttpURLConnection) url.openConnection();
-              }
-              connection.setRequestMethod("GET");
-
-              //Add cookie support
-              String cookie = CookieManager.getInstance().getCookie(source);
-              if(cookie != null)
-              {
-                connection.setRequestProperty("cookie", cookie);
-              }
-
-              connection.connect();
-
-              Log.d(LOG_TAG, "Download file: " + url);
-
-                connection.connect();
-
-                Log.d(LOG_TAG, "Download file:" + url);
-                InputStream inputStream = getInputStream(connection);
-
-                byte[] buffer = new byte[1024];
-                int bytesRead = 0;
-                long totalBytes = 0;
-                FileProgressResult progress = new FileProgressResult();
-
-                if (connection.getContentEncoding() == null) {
-                    // Only trust content-length header if no gzip etc
-                    progress.setLengthComputable(true);
-                    progress.setTotal(connection.getContentLength());
-                }
+            url = new URL(source);
+        } catch (MalformedURLException e) {
+            JSONObject error = createFileTransferError(INVALID_URL_ERR, 
source, target, 0);
+            Log.e(LOG_TAG, error.toString(), e);
+            callbackContext.sendPluginResult(new 
PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+            return;
+        }
+        final boolean useHttps = 
url.getProtocol().toLowerCase().equals("https");
+        
+        if (!webView.isUrlWhiteListed(source)) {
+            Log.w(LOG_TAG, "Source URL is not in white list: '" + source + 
"'");
+            JSONObject error = createFileTransferError(CONNECTION_ERR, source, 
target, 401);
+            callbackContext.sendPluginResult(new 
PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+            return;
+        }
 
-                FileOutputStream outputStream = new FileOutputStream(file);
-
-                // write bytes to file
-                while ((bytesRead = inputStream.read(buffer)) > 0) {
-                    outputStream.write(buffer, 0, bytesRead);
-                    totalBytes += bytesRead;
-                    if (objectId != null) {
-                        // Only send progress callbacks if the JS code sent us 
an object ID,
-                        // so we don't spam old versions with unrecognized 
callbacks.
-                        progress.setLoaded(totalBytes);
-                        PluginResult progressResult = new 
PluginResult(PluginResult.Status.OK, progress.toJSONObject());
-                        progressResult.setKeepCallback(true);
-                        callbackContext.sendPluginResult(progressResult);
-                    }
-                    synchronized (abortTriggered) {
-                        if (objectId != null && 
abortTriggered.contains(objectId)) {
-                            abortTriggered.remove(objectId);
-                            throw new AbortException("download aborted");
-                        }
-                    }
+        
+        final RequestContext context = new RequestContext(source, target, 
callbackContext);
+        synchronized (activeRequests) {
+            activeRequests.put(objectId, context);
+        }
+        
+        cordova.getThreadPool().execute(new Runnable() {
+            @Override
+            public void run() {
+                if (context.aborted) {
+                    return;
                 }
+                HttpURLConnection connection = null;
+                HostnameVerifier defaultHostnameVerifier = null;
 
-                outputStream.close();
-
-                Log.d(LOG_TAG, "Saved file: " + target);
-
-              // create FileEntry object
-              FileUtils fileUtil = new FileUtils();
-              JSONObject fileEntry = fileUtil.getEntry(file);
+                try {
 
-              // Revert back to the proper verifier and socket factories
-              if (trustEveryone && 
url.getProtocol().toLowerCase().equals("https")) {
-                  ((HttpsURLConnection) 
connection).setHostnameVerifier(defaultHostnameVerifier);
-                  
HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
-              }
+                    // create needed directories
+                    File file = getFileFromPath(target);
+                    file.getParentFile().mkdirs();
+        
+                    // connect to server
+                    // Open a HTTP connection to the URL based on protocol
+                    if (useHttps) {
+                        // Using standard HTTPS connection. Will not allow 
self signed certificate
+                        if (!trustEveryone) {
+                            connection = (HttpsURLConnection) 
url.openConnection();
+                        }
+                        // Use our HTTPS connection that blindly trusts 
everyone.
+                        // This should only be used in debug environments
+                        else {
+                            // Setup the HTTPS connection class to trust 
everyone
+                            trustAllHosts();
+                            HttpsURLConnection https = (HttpsURLConnection) 
url.openConnection();
+                            // Save the current hostnameVerifier
+                            defaultHostnameVerifier = 
https.getHostnameVerifier();
+                            // Setup the connection not to verify hostnames
+                            https.setHostnameVerifier(DO_NOT_VERIFY);
+                            connection = https;
+                        }
+                    }
+                    // Return a standard HTTP connection
+                    else {
+                          connection = (HttpURLConnection) 
url.openConnection();
+                    }
+    
+                    connection.setRequestMethod("GET");
+    
+                    //Add cookie support
+                    String cookie = 
CookieManager.getInstance().getCookie(source);
+                    if(cookie != null)
+                    {
+                        connection.setRequestProperty("cookie", cookie);
+                    }
+    
+                    connection.connect();
+    
+                    Log.d(LOG_TAG, "Download file:" + url);
 
-              return new PluginResult(PluginResult.Status.OK, fileEntry);
-            }
-            else
-            {
-                Log.w(LOG_TAG, "Source URL is not in white list: '" + source + 
"'");
-                JSONObject error = createFileTransferError(CONNECTION_ERR, 
source, target, 401);
-                return new PluginResult(PluginResult.Status.IO_EXCEPTION, 
error);
-            }
+                    FileProgressResult progress = new FileProgressResult();
+                    if (connection.getContentEncoding() == null) {
+                        // Only trust content-length header if no gzip etc
+                        progress.setLengthComputable(true);
+                        progress.setTotal(connection.getContentLength());
+                    }
+                    
+                    FileOutputStream outputStream = new FileOutputStream(file);
+                    InputStream inputStream = null;
+                    
+                    try {
+                        synchronized (context) {
+                            if (context.aborted) {
+                                throw new IOException("Request aborted");
+                            }
+                            inputStream = getInputStream(connection);
+                            context.currentInputStream = inputStream;
+                        }
+                        
+                        // write bytes to file
+                        byte[] buffer = new byte[MAX_BUFFER_SIZE];
+                        int bytesRead = 0;
+                        long totalBytes = 0;
+                        while ((bytesRead = inputStream.read(buffer)) > 0) {
+                            outputStream.write(buffer, 0, bytesRead);
+                            totalBytes += bytesRead;
+                            // Send a progress event.
+                            progress.setLoaded(totalBytes);
+                            PluginResult progressResult = new 
PluginResult(PluginResult.Status.OK, progress.toJSONObject());
+                            progressResult.setKeepCallback(true);
+                            context.sendPluginResult(progressResult);
+                        }
+                    } finally {
+                        safeClose(inputStream);
+                        safeClose(outputStream);
+                    }
+    
+                    Log.d(LOG_TAG, "Saved file: " + target);
+    
+                    // create FileEntry object
+                    FileUtils fileUtil = new FileUtils();
+                    JSONObject fileEntry = fileUtil.getEntry(file);
+                    
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.OK, fileEntry));
+                } catch (FileNotFoundException e) {
+                    JSONObject error = 
createFileTransferError(FILE_NOT_FOUND_ERR, source, target, connection);
+                    Log.e(LOG_TAG, error.toString(), e);
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+                } catch (IOException e) {
+                    JSONObject error = createFileTransferError(CONNECTION_ERR, 
source, target, connection);
+                    Log.e(LOG_TAG, error.toString(), e);
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+                } catch (JSONException e) {
+                    Log.e(LOG_TAG, e.getMessage(), e);
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.JSON_EXCEPTION));
+                } catch (Throwable e) {
+                    JSONObject error = createFileTransferError(CONNECTION_ERR, 
source, target, connection);
+                    Log.e(LOG_TAG, error.toString(), e);
+                    context.sendPluginResult(new 
PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+                } finally {
+                    synchronized (activeRequests) {
+                        activeRequests.remove(objectId);
+                    }
 
-        } catch (AbortException e) {
-            JSONObject error = createFileTransferError(ABORTED_ERR, source, 
target, connection);
-            return new PluginResult(PluginResult.Status.ERROR, error);
-        } catch (FileNotFoundException e) {
-            JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, 
source, target, connection);
-            Log.d(LOG_TAG, "I got a file not found exception");
-            Log.e(LOG_TAG, error.toString(), e);
-            return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-        } catch (MalformedURLException e) {
-            JSONObject error = createFileTransferError(INVALID_URL_ERR, 
source, target, connection);
-            Log.e(LOG_TAG, error.toString(), e);
-            return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-        } catch (Exception e) {  // IOException, JSONException, NullPointer
-            JSONObject error = createFileTransferError(CONNECTION_ERR, source, 
target, connection);
-            Log.e(LOG_TAG, error.toString(), e);
-            return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
-        } finally {
-            if (connection != null) {
-                connection.disconnect();
+                    if (connection != null) {
+                        // Revert back to the proper verifier and socket 
factories
+                        if (trustEveryone && 
url.getProtocol().toLowerCase().equals("https")) {
+                            ((HttpsURLConnection) 
connection).setHostnameVerifier(defaultHostnameVerifier);
+                            
HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
+                        }
+    
+                        connection.disconnect();
+                    }
+                }
             }
-        }
+        });
     }
 
     /**
@@ -727,20 +795,29 @@ public class FileTransfer extends CordovaPlugin {
 
     /**
      * Abort an ongoing upload or download.
-     *
-     * @param args          args
      */
-    private PluginResult abort(JSONArray args) {
-        String objectId;
-        try {
-            objectId = args.getString(0);
-        } catch (JSONException e) {
-            Log.d(LOG_TAG, "Missing objectId");
-            return new PluginResult(PluginResult.Status.JSON_EXCEPTION, 
"Missing objectId");
+    private void abort(String objectId) {
+        final RequestContext context;
+        synchronized (activeRequests) {
+            context = activeRequests.remove(objectId);
         }
-        synchronized (abortTriggered) {
-            abortTriggered.add(objectId);
+        if (context != null) {
+            // Trigger the abort callback immediately to minimize latency 
between it and abort() being called.
+            JSONObject error = createFileTransferError(ABORTED_ERR, 
context.source, context.target, -1);
+            synchronized (context) {
+                context.sendPluginResult(new 
PluginResult(PluginResult.Status.ERROR, error));
+                context.aborted = true;
+            }
+            // Closing the streams can block, so execute on a background 
thread.
+            cordova.getThreadPool().execute(new Runnable() {
+                @Override
+                public void run() {
+                    synchronized (context) {
+                        safeClose(context.currentInputStream);
+                        safeClose(context.currentOutputStream);
+                    }
+                }
+            });
         }
-        return new PluginResult(PluginResult.Status.OK);
     }
 }

Reply via email to