diff --git a/framework/src/org/apache/cordova/FileProgressResult.java b/framework/src/org/apache/cordova/FileProgressResult.java new file mode 100644 index 00000000..d9811755 --- /dev/null +++ b/framework/src/org/apache/cordova/FileProgressResult.java @@ -0,0 +1,63 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +package org.apache.cordova; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Encapsulates in-progress status of uploading or downloading a file to a remote server. + */ +public class FileProgressResult { + + private boolean lengthComputable = false; // declares whether total is known + private long loaded = 0; // bytes sent so far + private long total = 0; // bytes total, if known + + public boolean getLengthComputable() { + return lengthComputable; + } + + public void setLengthComputable(boolean computable) { + this.lengthComputable = computable; + } + + public long getLoaded() { + return loaded; + } + + public void setLoaded(long bytes) { + this.loaded = bytes; + } + + public long getTotal() { + return total; + } + + public void setTotal(long bytes) { + this.total = bytes; + } + + public JSONObject toJSONObject() throws JSONException { + return new JSONObject( + "{loaded:" + loaded + + ",total:" + total + + ",lengthComputable:" + (lengthComputable ? "true" : "false") + "}"); + } +} diff --git a/framework/src/org/apache/cordova/FileTransfer.java b/framework/src/org/apache/cordova/FileTransfer.java index 881caa54..ebce784f 100644 --- a/framework/src/org/apache/cordova/FileTransfer.java +++ b/framework/src/org/apache/cordova/FileTransfer.java @@ -32,6 +32,7 @@ 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.Iterator; import javax.net.ssl.HostnameVerifier; @@ -63,29 +64,44 @@ public class FileTransfer extends Plugin { public static int FILE_NOT_FOUND_ERR = 1; public static int INVALID_URL_ERR = 2; public static int CONNECTION_ERR = 3; + public static int ABORTED_ERR = 4; + + private static HashSet abortTriggered = new HashSet(); private SSLSocketFactory defaultSSLSocketFactory = null; private HostnameVerifier defaultHostnameVerifier = null; + static class AbortException extends Exception { + public AbortException(String str) { + super(str); + } + } + /* (non-Javadoc) * @see org.apache.cordova.api.Plugin#execute(java.lang.String, org.json.JSONArray, java.lang.String) */ @Override public PluginResult execute(String action, JSONArray args, String callbackId) { - 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"); - } + 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"); + } - if (action.equals("upload")) { - return upload(URLDecoder.decode(source), target, args); - } else if (action.equals("download")) { - return download(source, target, args.optBoolean(2)); + if (action.equals("upload")) { + return upload(URLDecoder.decode(source), target, args, callbackId); + } else if (action.equals("download")) { + String objectId = args.getString(2); + boolean trustEveryone = args.optBoolean(3); + return download(source, target, trustEveryone, objectId, callbackId); + } + } else if (action.equals("abort")) { + return abort(args); } else { return new PluginResult(PluginResult.Status.INVALID_ACTION); } @@ -96,6 +112,7 @@ public class FileTransfer extends Plugin { * @param source Full path of the file on the file system * @param target URL of the server to receive the file * @param args JSON Array of args + * @param callbackId callback id for optional progress reports * * args[2] fileKey Name of file request parameter * args[3] fileName File name to be used on server @@ -103,7 +120,7 @@ public class FileTransfer extends Plugin { * 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) { + private PluginResult upload(String source, String target, JSONArray args, String callbackId) { Log.d(LOG_TAG, "upload " + source + " to " + target); HttpURLConnection conn = null; @@ -121,6 +138,7 @@ public class FileTransfer extends Plugin { 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); @@ -129,9 +147,11 @@ public class FileTransfer extends Plugin { 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 FileInputStream fileInputStream = (FileInputStream) getPathFromUri(source); @@ -285,6 +305,21 @@ public class FileTransfer extends Plugin { bytesAvailable = fileInputStream.available(); bufferSize = Math.min(bytesAvailable, maxBufferSize); bytesRead = fileInputStream.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. + Log.d(LOG_TAG, "****** About to send a progress result from upload"); + progress.setLoaded(totalBytes); + PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject()); + progressResult.setKeepCallback(true); + success(progressResult, callbackId); + } + synchronized (abortTriggered) { + if (objectId != null && abortTriggered.contains(objectId)) { + abortTriggered.remove(objectId); + throw new AbortException("upload aborted"); + } + } } // send multipart form data necessary after file data... @@ -342,6 +377,10 @@ public class FileTransfer extends Plugin { } 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); + Log.e(LOG_TAG, error.toString(), e); + return new PluginResult(PluginResult.Status.ERROR, error); } catch (Throwable t) { // Shouldn't happen, but will JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn); @@ -459,7 +498,7 @@ public class FileTransfer extends Plugin { * @param target Full path of the file on the file system * @return JSONObject the downloaded file */ - private PluginResult download(String source, String target, boolean trustEveryone) { + private PluginResult download(String source, String target, boolean trustEveryone, String objectId, String callbackId) { Log.d(LOG_TAG, "download " + source + " to " + target); HttpURLConnection connection = null; @@ -523,12 +562,36 @@ public class FileTransfer extends Plugin { 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()); + } 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. + Log.d(LOG_TAG, "****** About to send a progress result from download"); + progress.setLoaded(totalBytes); + PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject()); + progressResult.setKeepCallback(true); + success(progressResult, callbackId); + } + synchronized (abortTriggered) { + if (objectId != null && abortTriggered.contains(objectId)) { + abortTriggered.remove(objectId); + throw new AbortException("download aborted"); + } + } } outputStream.close(); @@ -621,4 +684,23 @@ public class FileTransfer extends Plugin { return file; } + + /** + * 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"); + } + synchronized (abortTriggered) { + abortTriggered.add(objectId); + } + return new PluginResult(PluginResult.Status.OK); + } } diff --git a/framework/src/org/apache/cordova/FileUploadResult.java b/framework/src/org/apache/cordova/FileUploadResult.java index 36fae93f..b556869e 100644 --- a/framework/src/org/apache/cordova/FileUploadResult.java +++ b/framework/src/org/apache/cordova/FileUploadResult.java @@ -29,6 +29,7 @@ public class FileUploadResult { private long bytesSent = 0; // bytes sent private int responseCode = -1; // HTTP response code private String response = null; // HTTP response + private String objectId = null; // FileTransfer object id public long getBytesSent() { return bytesSent; @@ -54,10 +55,19 @@ public class FileUploadResult { this.response = response; } + public String getObjectId() { + return objectId; + } + + public void setObjectId(String objectId) { + this.objectId = objectId; + } + public JSONObject toJSONObject() throws JSONException { return new JSONObject( "{bytesSent:" + bytesSent + ",responseCode:" + responseCode + - ",response:" + JSONObject.quote(response) + "}"); + ",response:" + JSONObject.quote(response) + + ",objectId:" + JSONObject.quote(objectId) + "}"); } }