diff --git a/android/jni/com/mapswithme/platform/HttpUploader.cpp b/android/jni/com/mapswithme/platform/HttpUploader.cpp index 67fdea35ca..f86b0ee40b 100644 --- a/android/jni/com/mapswithme/platform/HttpUploader.cpp +++ b/android/jni/com/mapswithme/platform/HttpUploader.cpp @@ -63,6 +63,14 @@ HttpUploader::Result HttpUploader::Upload() const jni::ScopedLocalRef const result( env, env->CallObjectMethod(httpUploaderObject, uploadId)); + if (jni::HandleJavaException(env)) + { + Result invalidResult; + invalidResult.m_httpCode = -1; + invalidResult.m_description = "Unhandled exception during upload is encountered!"; + return invalidResult; + } + return ToNativeResult(env, result); } } // namespace platform diff --git a/android/src/com/mapswithme/util/HttpClient.java b/android/src/com/mapswithme/util/HttpClient.java index 67b71dc798..f51f6598fb 100644 --- a/android/src/com/mapswithme/util/HttpClient.java +++ b/android/src/com/mapswithme/util/HttpClient.java @@ -65,7 +65,7 @@ public final class HttpClient HttpURLConnection connection = null; - LOGGER.d(TAG, "Connecting to " + makeUrlSafe(p.url)); + LOGGER.d(TAG, "Connecting to " + Utils.makeUrlSafe(p.url)); try { @@ -147,7 +147,7 @@ public final class HttpClient // GET data from the server or receive response body p.httpResponseCode = connection.getResponseCode(); LOGGER.d(TAG, "Received HTTP " + p.httpResponseCode + " from server, content encoding = " - + connection.getContentEncoding() + ", for request = " + makeUrlSafe(p.url)); + + connection.getContentEncoding() + ", for request = " + Utils.makeUrlSafe(p.url)); if (p.httpResponseCode >= 300 && p.httpResponseCode < 400) p.receivedUrl = connection.getHeaderField("Location"); @@ -239,11 +239,6 @@ public final class HttpClient return in; } - private static String makeUrlSafe(@NonNull final String url) - { - return url.replaceAll("(token|password|key)=([^&]+)", "***"); - } - private static class Params { public void setHeaders(@NonNull KeyValue[] array) diff --git a/android/src/com/mapswithme/util/HttpUploader.java b/android/src/com/mapswithme/util/HttpUploader.java index 44b67d6942..315f117b4c 100644 --- a/android/src/com/mapswithme/util/HttpUploader.java +++ b/android/src/com/mapswithme/util/HttpUploader.java @@ -1,13 +1,38 @@ package com.mapswithme.util; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import com.mapswithme.maps.BuildConfig; +import com.mapswithme.util.log.Logger; +import com.mapswithme.util.log.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -final class HttpUploader +public final class HttpUploader { + private static final Logger LOGGER = LoggerFactory.INSTANCE.getLogger(LoggerFactory.Type.NETWORK); + private static final String TAG = HttpUploader.class.getSimpleName(); + private static final String USER_AGENT = "Mapsme Android"; + private static final String LINE_FEED = "\r\n"; + private static final String CHARSET = "UTF-8"; + private static final int BUFFER = 8192; + private static final int STATUS_CODE_UNKNOWN = -1; @NonNull private final String mMethod; @NonNull @@ -20,24 +45,177 @@ final class HttpUploader private final String mFileKey; @NonNull private final String mFilePath; + @NonNull + private final String mBoundary; - @SuppressWarnings("unused") - private HttpUploader(@NonNull String method, @NonNull String url, @NonNull KeyValue[] params, - @NonNull KeyValue[] headers, @NonNull String fileKey, @NonNull String filePath) + public HttpUploader(@NonNull String method, @NonNull String url, @NonNull KeyValue[] params, + @NonNull KeyValue[] headers, @NonNull String fileKey, @NonNull String filePath) { mMethod = method; mUrl = url; - mParams = new ArrayList<>(Arrays.asList(params)); - mHeaders = new ArrayList<>(Arrays.asList(headers)); mFileKey = fileKey; mFilePath = filePath; + mBoundary = "------------------------" + System.currentTimeMillis(); + mParams = new ArrayList<>(Arrays.asList(params)); + mHeaders = new ArrayList<>(Arrays.asList(headers)); } - @SuppressWarnings("unused") - private Result upload() + public Result upload() { - // Dummy. Error code 200 - Http OK. - return new Result(200, ""); + int status; + String message; + PrintWriter writer = null; + BufferedReader reader = null; + HttpURLConnection connection = null; + try + { + URL url = new URL(mUrl); + connection = (HttpURLConnection) url.openConnection(); + connection.setUseCaches(false); + connection.setRequestMethod(mMethod); + connection.setDoOutput(mMethod.equals("POST")); + connection.setDoInput(true); + + long fileSize = StorageUtils.getFileSize(mFilePath); + StringBuilder paramsBuilder = new StringBuilder(); + fillBodyParams(paramsBuilder); + File file = new File(mFilePath); + fillFileParams(paramsBuilder, mFileKey, file); + int endPartSize = LINE_FEED.length() + "--".length() + mBoundary.length() + + "--".length() + LINE_FEED.length(); + long bodyLength = paramsBuilder.toString().length() + fileSize + endPartSize; + setHeaders(connection, bodyLength); + + OutputStream outputStream = connection.getOutputStream(); + writer = new PrintWriter(new OutputStreamWriter(outputStream, CHARSET)); + writeParams(writer, paramsBuilder); + writeFileContent(outputStream, writer, file); + + status = connection.getResponseCode(); + LOGGER.d(TAG, "Upload bookmarks status code: " + status); + reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + message = readResponse(reader); + LOGGER.d(TAG, "Upload bookmarks response: " + message); + } + catch (IOException e) + { + status = STATUS_CODE_UNKNOWN; + message = "I/O exception '" + Utils.makeUrlSafe(mUrl) + "'"; + if (connection != null) + { + String errMsg = readErrorResponse(connection); + if (!TextUtils.isEmpty(errMsg)) + message = errMsg; + } + LOGGER.e(TAG, message, e); + } + finally + { + Utils.closeStream(writer); + Utils.closeStream(reader); + if (connection != null) + connection.disconnect(); + } + return new Result(status, message); + } + + @NonNull + private String readResponse(@NonNull BufferedReader reader) + throws IOException + { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) + response.append(line); + return response.toString(); + } + + @Nullable + private String readErrorResponse(@NonNull HttpURLConnection connection) + { + BufferedReader reader = null; + try + { + InputStream errStream = connection.getErrorStream(); + if (errStream == null) + return null; + + reader = new BufferedReader( + new InputStreamReader(connection.getErrorStream())); + return readResponse(reader); + } + catch (IOException e) + { + LOGGER.e(TAG, "Failed to read a error stream."); + } + finally + { + Utils.closeStream(reader); + } + return null; + } + + private void writeParams(@NonNull PrintWriter writer, @NonNull StringBuilder paramsBuilder) + { + writer.append(paramsBuilder); + writer.flush(); + } + + private void setHeaders(@NonNull URLConnection connection, long bodyLength) + { + mHeaders.add(new KeyValue("User-Agent", USER_AGENT)); + mHeaders.add(new KeyValue("App-Version", BuildConfig.VERSION_NAME)); + mHeaders.add(new KeyValue("Content-Type", "multipart/form-data; boundary=" + mBoundary)); + mHeaders.add(new KeyValue("Content-Length", String.valueOf(bodyLength))); + for (KeyValue header : mHeaders) + connection.setRequestProperty(header.mKey, header.mValue); + } + + private void fillBodyParams(@NonNull StringBuilder builder) + { + for (KeyValue field : mParams) + addParam(builder, field.mKey, field.mValue); + } + + private void addParam(@NonNull StringBuilder builder, @NonNull String key, @NonNull String value) + { + builder.append("--").append(mBoundary).append(LINE_FEED); + builder.append("Content-Disposition: form-data; name=\"") + .append(key) + .append("\"") + .append(LINE_FEED); + builder.append(LINE_FEED); + builder.append(value).append(LINE_FEED); + } + + private void fillFileParams(@NonNull StringBuilder builder, @NonNull String fieldName, + @NonNull File uploadFile) + { + String fileName = uploadFile.getName(); + builder.append("--").append(mBoundary).append(LINE_FEED); + builder.append("Content-Disposition: form-data; name=\"") + .append(fieldName) + .append("\"; filename=\"") + .append(fileName) + .append("\"") + .append(LINE_FEED); + builder.append("Content-Type: ").append(URLConnection.guessContentTypeFromName(fileName)) + .append(LINE_FEED); + builder.append(LINE_FEED); + } + + private void writeFileContent(@NonNull OutputStream outputStream, @NonNull PrintWriter writer, + @NonNull File uploadFile) throws IOException + { + FileInputStream inputStream = new FileInputStream(uploadFile); + int size = Math.min((int) uploadFile.length(), BUFFER); + byte[] buffer = new byte[size]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) + outputStream.write(buffer, 0, bytesRead); + Utils.closeStream(inputStream); + writer.append(LINE_FEED).append("--").append(mBoundary).append("--").append(LINE_FEED); + writer.flush(); } private static class Result diff --git a/android/src/com/mapswithme/util/KeyValue.java b/android/src/com/mapswithme/util/KeyValue.java index d4d7d9fc90..19c7556424 100644 --- a/android/src/com/mapswithme/util/KeyValue.java +++ b/android/src/com/mapswithme/util/KeyValue.java @@ -2,9 +2,9 @@ package com.mapswithme.util; import android.support.annotation.NonNull; -final class KeyValue +public final class KeyValue { - KeyValue(@NonNull String key, @NonNull String value) + public KeyValue(@NonNull String key, @NonNull String value) { mKey = key; mValue = value; diff --git a/android/src/com/mapswithme/util/StorageUtils.java b/android/src/com/mapswithme/util/StorageUtils.java index 91361b2efd..e6dfa7dfcb 100644 --- a/android/src/com/mapswithme/util/StorageUtils.java +++ b/android/src/com/mapswithme/util/StorageUtils.java @@ -83,4 +83,10 @@ public class StorageUtils File file = new File(zipFile); return file.isFile() && file.exists() ? zipFile : null; } + + public static long getFileSize(@NonNull String path) + { + File file = new File(path); + return file.length(); + } } diff --git a/android/src/com/mapswithme/util/Utils.java b/android/src/com/mapswithme/util/Utils.java index 0164bedc14..caeaffa9f5 100644 --- a/android/src/com/mapswithme/util/Utils.java +++ b/android/src/com/mapswithme/util/Utils.java @@ -60,7 +60,7 @@ public class Utils private Utils() {} - public static void closeStream(Closeable stream) + public static void closeStream(@Nullable Closeable stream) { if (stream != null) { @@ -440,6 +440,11 @@ public class Utils return text; } + static String makeUrlSafe(@NonNull final String url) + { + return url.replaceAll("(token|password|key)=([^&]+)", "***"); + } + @StringRes public static int getStringIdByKey(@NonNull Context context, @NonNull String key) {