diff --git a/olp-cpp-sdk-core/cmake/android.cmake b/olp-cpp-sdk-core/cmake/android.cmake index 53a2bc584..88ae66376 100644 --- a/olp-cpp-sdk-core/cmake/android.cmake +++ b/olp-cpp-sdk-core/cmake/android.cmake @@ -41,11 +41,28 @@ get_android_jar_path(CMAKE_JAVA_INCLUDE_PATH ANDROID_SDK_ROOT ANDROID_PLATFORM) set(OLP_SDK_NETWORK_VERSION 0.0.1) set(OLP_SDK_ANDROID_HTTP_CLIENT_JAR OlpHttpClient) +set(MAVEN_DEPS_OUTPUT ${CMAKE_BINARY_DIR}/maven-deps) +set(ALL_MAVEN_PACKAGES + ${MAVEN_DEPS_OUTPUT}/okhttp-4.12.0.jar + ${MAVEN_DEPS_OUTPUT}/okio-3.10.2.jar +) + +add_custom_command( + OUTPUT ${ALL_MAVEN_PACKAGES} + COMMAND mvn -f ${CMAKE_CURRENT_LIST_DIR}/pom.xml dependency:copy-dependencies -DoutputDirectory=${MAVEN_DEPS_OUTPUT} + COMMENT "Downloading Maven packages" +) + +add_custom_target(olp-cpp-sdk-core.maven DEPENDS ${ALL_MAVEN_PACKAGES}) + add_jar(${OLP_SDK_ANDROID_HTTP_CLIENT_JAR} - SOURCES ${CMAKE_CURRENT_LIST_DIR}/../src/http/android/HttpClient.java + SOURCES ${CMAKE_CURRENT_LIST_DIR}/../src/http/android/OlpHttpClient.java VERSION ${OLP_SDK_NETWORK_VERSION} + INCLUDE_JARS ${ALL_MAVEN_PACKAGES} ) +add_dependencies(${OLP_SDK_ANDROID_HTTP_CLIENT_JAR} olp-cpp-sdk-core.maven) + # add_jar() doesn't add the symlink to the version automatically under Windows if (WIN32) include(SymlinkHelpers) diff --git a/olp-cpp-sdk-core/cmake/pom.xml b/olp-cpp-sdk-core/cmake/pom.xml new file mode 100644 index 000000000..b701d993f --- /dev/null +++ b/olp-cpp-sdk-core/cmake/pom.xml @@ -0,0 +1,19 @@ + + 4.0.0 + com.here.olp + download-jars + 1.0-SNAPSHOT + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + com.squareup.okio + okio + 3.10.2 + + + diff --git a/olp-cpp-sdk-core/src/http/android/HttpClient.java b/olp-cpp-sdk-core/src/http/android/HttpClient.java deleted file mode 100644 index 99e6b6360..000000000 --- a/olp-cpp-sdk-core/src/http/android/HttpClient.java +++ /dev/null @@ -1,615 +0,0 @@ -/* - * Copyright (C) 2019-2025 HERE Europe B.V. - * - * Licensed 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. - * - * SPDX-License-Identifier: Apache-2.0 - * License-Filename: LICENSE - */ - -package com.here.olp.network; - -import android.os.AsyncTask; -import android.os.OperationCanceledException; -import android.util.Log; - -import java.io.BufferedInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.WeakReference; -import java.net.HttpURLConnection; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.ProtocolException; -import java.net.Proxy; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.net.URLConnection; -import java.net.UnknownHostException; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.net.ssl.SSLException; - -public class HttpClient { - private static final String LOGTAG = "HttpClient"; - - // The replica of http/NetworkType.h error codes - public static final int IO_ERROR = -1; - public static final int AUTHORIZATION_ERROR = -2; - public static final int INVALID_URL_ERROR = -3; - public static final int OFFLINE_ERROR = -4; - public static final int CANCELLED_ERROR = -5; - public static final int TIMEOUT_ERROR = -7; - public static final int THREAD_POOL_SIZE = 8; - - // The raw pointer to the C++ NetworkAndroid class - private long nativePtr; - - public enum HttpVerb { - GET, - POST, - HEAD, - PUT, - DELETE, - PATCH, - OPTIONS - }; - - public static HttpVerb toHttpVerb(int verb) { - switch (verb) { - case 0: - return HttpVerb.GET; - case 1: - return HttpVerb.POST; - case 2: - return HttpVerb.HEAD; - case 3: - return HttpVerb.PUT; - case 4: - return HttpVerb.DELETE; - case 5: - return HttpVerb.PATCH; - case 6: - return HttpVerb.OPTIONS; - default: - return HttpVerb.GET; - } - } - - /** Class to hold the request's data, which will be used by HttpUrlConnection object. */ - public final class Request { - public Request( - String url, - HttpVerb verb, - long requestId, - int connectionTimeout, - int requestTimeout, - String[] headers, - byte[] postData, - String proxyServer, - int proxyPort, - int proxyType) { - this.url = url; - this.verb = verb; - this.requestId = requestId; - this.connectionTimeout = connectionTimeout; - this.requestTimeout = requestTimeout; - this.headers = headers; - this.postData = postData; - this.proxyServer = proxyServer; - this.proxyPort = proxyPort; - - switch (proxyType) { - case 0: - this.proxyType = Proxy.Type.DIRECT; - break; - case 1: - this.proxyType = Proxy.Type.HTTP; - break; - case 2: - Log.w(LOGTAG, "HttpClient::Request(): Unsupported proxy version (" + proxyType + "). Falling back to HTTP(1)"); - this.proxyType = Proxy.Type.HTTP; - break; - case 3: - this.proxyType = Proxy.Type.SOCKS; - break; - case 4: - case 5: - case 6: - Log.w(LOGTAG, "HttpClient::Request(): Unsupported proxy version (" + proxyType + "). Falling back to SOCKS4(3)"); - this.proxyType = Proxy.Type.SOCKS; - break; - default: - Log.w(LOGTAG, "HttpClient::Request(): Unsupported proxy version (" + proxyType + "). Falling back to HTTP(1)"); - this.proxyType = Proxy.Type.HTTP; - break; - } - } - - public final String url() { - return this.url; - } - - public final HttpVerb verb() { - return this.verb; - } - - public final long requestId() { - return this.requestId; - } - - public final int connectTimeout() { - return this.connectionTimeout; - } - - public final int requestTimeout() { - return this.requestTimeout; - } - - public final String[] headers() { - return this.headers; - } - - public final byte[] postData() { - return this.postData; - } - - public final String proxyServer() { - return this.proxyServer; - } - - public final int proxyPort() { - return this.proxyPort; - } - - public final Proxy.Type proxyType() { - return this.proxyType; - } - - public final boolean hasProxy() { - return (this.proxyServer != null) && !this.proxyServer.isEmpty(); - } - - public final boolean noProxy() { - return (hasProxy() && this.proxyServer.equals("No")) || this.proxyType == Proxy.Type.DIRECT; - } - - private final String url; - private final long requestId; - private final int connectionTimeout; - private final int requestTimeout; - private final String[] headers; - private final byte[] postData; - private final HttpVerb verb; - private final String proxyServer; - private final int proxyPort; - private final Proxy.Type proxyType; - } - - /** - * Task class sends the request HttpUrlConnection and responsible for handling response as well. - */ - private static class HttpTask extends AsyncTask { - private final WeakReference weakReference; - private final AtomicBoolean cancelled = new AtomicBoolean(false); - - public synchronized void cancelTask() { - this.cancelled.set(true); - } - - private HttpTask(HttpClient client) { - weakReference = new WeakReference<>(client); - } - - @Override - protected Void doInBackground(Request... requests) { - for (Request request : requests) { - HttpURLConnection httpConn = null; - int uploadedContentSize = 0; - int downloadContentSize = 0; - boolean downloadContentSizePresent = false; - - try { - boolean isDone = false; - final URL url = new URL(request.url()); - - do { - final HttpClient httpClient = weakReference.get(); - if (httpClient == null) { - return null; - } - - URLConnection conn; - if (request.hasProxy()) { - if (request.noProxy()) { - conn = url.openConnection(Proxy.NO_PROXY); - } else { - conn = - url.openConnection( - new Proxy( - request.proxyType(), - new InetSocketAddress(request.proxyServer(), request.proxyPort()))); - } - } else { - conn = url.openConnection(); - } - - if (conn instanceof HttpURLConnection) { - httpConn = (HttpURLConnection) conn; - } - - if (httpConn != null) { - switch (request.verb()) { - case HEAD: - httpConn.setRequestMethod("HEAD"); - break; - case PUT: - httpConn.setRequestMethod("PUT"); - break; - case DELETE: - httpConn.setRequestMethod("DELETE"); - break; - case PATCH: - httpConn.setRequestMethod("PATCH"); - break; - case OPTIONS: - httpConn.setRequestMethod("OPTIONS"); - break; - default: - httpConn.setRequestMethod("GET"); - break; - } - } - - String[] headers = request.headers(); - boolean useEtag = false; - boolean userSetConnection = false; - if (headers != null) { - for (int j = 0; (j + 1) < headers.length; j += 2) { - conn.addRequestProperty(headers[j], headers[j + 1]); - if (headers[j].compareToIgnoreCase("If-None-Match") == 0) { - useEtag = true; - } - if (headers[j].compareToIgnoreCase("Connection") == 0) { - userSetConnection = true; - } - } - } - - conn.setUseCaches(false); - conn.setConnectTimeout(request.connectTimeout() * 1000); - conn.setReadTimeout(request.requestTimeout() * 1000); - if (request.verb() != HttpVerb.HEAD && httpConn != null) { - if (request.postData() != null) { - httpConn.setFixedLengthStreamingMode(request.postData().length); - } else { - httpConn.setChunkedStreamingMode(8 * 1024); - } - } - // Android Issue 24672: workaround - if (android.os.Build.VERSION.SDK_INT < 21) { - if (request.verb() == HttpVerb.HEAD || useEtag) - conn.setRequestProperty("Accept-Encoding", ""); - } - - // Connection-Close setting causes extensive ~3-4 times delay. - // Keep user's connection setting if set explicitly. - if (!userSetConnection) { - // Fix too many open files issues on - // JellyBean (Android <= 4.3) and Android 6.x devices JellyBean 18 - - // Android 4.3, Marshmallow 23 - Android 6.x - if (android.os.Build.VERSION.SDK_INT <= 23) - conn.setRequestProperty("Connection", "Close"); - } - - uploadedContentSize += calculateHeadersSize(conn.getRequestProperties()); - - conn.setDoInput(true); - - // Do POST if needed - if (request.postData() != null) { - conn.setDoOutput(true); - conn.getOutputStream().write(request.postData()); - uploadedContentSize += request.postData().length; - } else { - conn.setDoOutput(false); - } - - // Wait for status response - int status = 0; - String error = ""; - if (httpConn != null) { - try { - status = httpConn.getResponseCode(); - error = httpConn.getResponseMessage(); - } catch (SocketTimeoutException | UnknownHostException e) { - throw e; - } - } - - checkCancelled(); - - // Read headers - String contentType = conn.getHeaderField("Content-Type"); - if (contentType == null) { - contentType = ""; - } - - // Parse Content-Range if available - long offset = 0; - String httpHeader = conn.getHeaderField("Content-Range"); - if (httpHeader != null) { - int index = httpHeader.indexOf("bytes "); - if (index >= 0) { - index += 6; - int endIndex = httpHeader.indexOf('-', index); - try { - String rangeStr; - if (endIndex > index) rangeStr = httpHeader.substring(index, endIndex); - else rangeStr = httpHeader.substring(index); - offset = Long.parseLong(rangeStr); - } catch (Exception e) { - Log.d(LOGTAG, "parse offset: " + e); - } - } - } - - downloadContentSize += calculateHeadersSize(conn.getHeaderFields()); - - int contentSize = conn.getContentLength(); - if(contentSize > 0){ - downloadContentSize += contentSize; - downloadContentSizePresent = true; - } - - // Get all the headers of the response - int headersCount = 0; - while (conn.getHeaderFieldKey(headersCount) != null) { - headersCount++; - } - - String[] headersArray = new String[2 * headersCount]; - for (int j = 0; j < headersCount; j++) { - headersArray[2 * j] = conn.getHeaderFieldKey(j); - headersArray[2 * j + 1] = conn.getHeaderField(j); - } - - checkCancelled(); - - httpClient.headersCallback(request.requestId(), headersArray); - httpClient.dateAndOffsetCallback(request.requestId(), 0l, offset); - - // Do the input phase - InputStream in = null; - try { - try { - in = new BufferedInputStream(conn.getInputStream()); - } catch (FileNotFoundException e) { - // error occurred, continuing with error stream - if (httpConn != null) { - in = new BufferedInputStream(httpConn.getErrorStream()); - } else { - throw e; - } - } - int len; - byte[] buffer = new byte[8 * 1024]; - - while ((len = in.read(buffer)) >= 0) { - checkCancelled(); - httpClient.dataCallback(request.requestId(), buffer, len); - if(!downloadContentSizePresent){ - downloadContentSize += len; - } - } - } - // Error handling: - catch (FileNotFoundException e) { - // ensure that the status has been set, then complete the request - if (status == 0) { - throw e; - } - } catch (ProtocolException e) { - if (status != HttpURLConnection.HTTP_NOT_MODIFIED - && status != HttpURLConnection.HTTP_NO_CONTENT) { - throw e; - } - } catch (SocketTimeoutException e) { - throw e; - } - - checkCancelled(); - - // The request is completed and not cancelled - // Notifies the native (C++) side that request was completed - isDone = true; - httpClient.completeRequest(request.requestId(), status, uploadedContentSize, downloadContentSize, error, contentType); - } while (!isDone); - } catch (SSLException e) { - completeErrorRequest(request.requestId(), IO_ERROR, uploadedContentSize, downloadContentSize, "SSL connection failed: " + e); - } catch (MalformedURLException e) { - completeErrorRequest(request.requestId(), INVALID_URL_ERROR, uploadedContentSize, downloadContentSize, "The provided URL is not valid: " + e); - } catch (OperationCanceledException e) { - completeErrorRequest(request.requestId(), CANCELLED_ERROR, uploadedContentSize, downloadContentSize, "Cancelled: " + e); - } catch (SocketTimeoutException e) { - completeErrorRequest(request.requestId(), TIMEOUT_ERROR, uploadedContentSize, downloadContentSize, "Timed out: " + e); - } catch (UnknownHostException e) { - completeErrorRequest(request.requestId(), OFFLINE_ERROR, uploadedContentSize, downloadContentSize, "The device has no internet connectivity: " + e); - } catch (Exception e) { - Log.e(LOGTAG, "HttpClient::HttpTask::run exception: " + e); - e.printStackTrace(); - completeErrorRequest(request.requestId(), IO_ERROR, uploadedContentSize, downloadContentSize, e.toString()); - } finally { - uploadedContentSize = 0; - downloadContentSize = 0; - cleanup(httpConn); - } - } - - return null; - } - - private void completeErrorRequest(long requestId, int status, int uploadedBytes, int downloadedBytes, String error) { - final HttpClient httpClient = weakReference.get(); - if (httpClient != null) { - httpClient.completeRequest(requestId, status, uploadedBytes, downloadedBytes, error, ""); - } - } - - private final void checkCancelled() { - if (this.cancelled.get()) { - throw new OperationCanceledException(); - } - } - - private final void cleanup(HttpURLConnection httpConn) { - if (httpConn == null) { - return; - } - - if (httpConn.getDoOutput()) { - try { - if (httpConn.getOutputStream() != null) { - httpConn.getOutputStream().flush(); - } - } catch (IOException e) { - // no-op - } - } - - try { - clearInputStream(httpConn.getInputStream()); - } catch (IOException e) { - // no-op - } - - try { - clearInputStream(httpConn.getErrorStream()); - } catch (IOException e) { - // no-op - } - - if (httpConn.getDoOutput()) { - try { - if (httpConn.getOutputStream() != null) httpConn.getOutputStream().close(); - } catch (IOException e) { - // no-op - } - } - - try { - if (httpConn.getInputStream() != null) httpConn.getInputStream().close(); - } catch (IOException e) { - // no-op - } - - try { - if (httpConn.getErrorStream() != null) httpConn.getErrorStream().close(); - } catch (IOException e) { - // no-op - } - - httpConn.disconnect(); - } - - private final void clearInputStream(InputStream stream) throws IOException { - if (stream == null) { - return; - } - - final byte[] buffer = new byte[8 * 1024]; - while (stream.read(buffer) > 0) { - // clear stream - } - } - - private final int calculateHeadersSize(Map> headers) throws IOException { - int size = 0; - for (Map.Entry> entry : headers.entrySet()) { - String header = entry.getKey(); - List values = entry.getValue(); - if(header != null) { - size += header.length(); - } - for (String value : values) { - if(value != null) { - size += value.length(); - } - } - } - return size; - } - }; - - private ExecutorService executor; - - public HttpClient() { - this.executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE); - } - - public void shutdown() { - if (this.executor != null) { - this.executor.shutdown(); - this.executor = null; - } - synchronized (this) { - // deinitialize the nativePtr to stop receiving scheduled events from Java to C++ - this.nativePtr = 0; - } - } - - public HttpTask send( - String url, - int httpMethod, - long requestId, - int connTimeout, - int reqTimeout, - String[] headers, - byte[] postData, - String proxyServer, - int proxyPort, - int proxyType) { - final Request request = - new Request( - url, - HttpClient.toHttpVerb(httpMethod), - requestId, - connTimeout, - reqTimeout, - headers, - postData, - proxyServer, - proxyPort, - proxyType); - final HttpTask task = new HttpTask(this); - task.executeOnExecutor(executor, request); - return task; - } - - // Native methods: - // Synchronization is required in order to provide thread-safe access to `nativePtr` - // Callback for completed request - private synchronized native void completeRequest( - long requestId, int status, int uploadedBytes, int downloadedBytes, String error, String contentType); - // Callback for data received - private synchronized native void dataCallback(long requestId, byte[] data, int len); - // Callback set date and offset - private synchronized native void dateAndOffsetCallback(long requestId, long date, long offset); - // Callback set date and offset - private synchronized native void headersCallback(long requestId, String[] headers); -} diff --git a/olp-cpp-sdk-core/src/http/android/NetworkAndroid.cpp b/olp-cpp-sdk-core/src/http/android/NetworkAndroid.cpp index 96007b479..6da08e71a 100644 --- a/olp-cpp-sdk-core/src/http/android/NetworkAndroid.cpp +++ b/olp-cpp-sdk-core/src/http/android/NetworkAndroid.cpp @@ -82,9 +82,10 @@ olp::http::NetworkAndroid* GetNetworkAndroidNativePtr(JNIEnv* env, * Callback to be called when response headers have been received */ extern "C" OLP_SDK_NETWORK_ANDROID_EXPORT void JNICALL -Java_com_here_olp_network_HttpClient_headersCallback(JNIEnv* env, jobject obj, - jlong request_id, - jobjectArray headers) { +Java_com_here_olp_network_OlpHttpClient_headersCallback(JNIEnv* env, + jobject obj, + jlong request_id, + jobjectArray headers) { auto network = olp::http::GetNetworkAndroidNativePtr(env, obj); if (!network) { OLP_SDK_LOG_WARNING( @@ -99,7 +100,7 @@ Java_com_here_olp_network_HttpClient_headersCallback(JNIEnv* env, jobject obj, * Callback to be called when a date header is received */ extern "C" OLP_SDK_NETWORK_ANDROID_EXPORT void JNICALL -Java_com_here_olp_network_HttpClient_dateAndOffsetCallback( +Java_com_here_olp_network_OlpHttpClient_dateAndOffsetCallback( JNIEnv* env, jobject obj, jlong request_id, jlong date, jlong offset) { auto network = olp::http::GetNetworkAndroidNativePtr(env, obj); if (!network) { @@ -116,9 +117,10 @@ Java_com_here_olp_network_HttpClient_dateAndOffsetCallback( * Callback to be called when a chunk of data is received */ extern "C" OLP_SDK_NETWORK_ANDROID_EXPORT void JNICALL -Java_com_here_olp_network_HttpClient_dataCallback(JNIEnv* env, jobject obj, - jlong request_id, - jbyteArray data, jint len) { +Java_com_here_olp_network_OlpHttpClient_dataCallback(JNIEnv* env, jobject obj, + jlong request_id, + jbyteArray data, + jint len) { auto network = olp::http::GetNetworkAndroidNativePtr(env, obj); if (!network) { OLP_SDK_LOG_WARNING( @@ -133,7 +135,7 @@ Java_com_here_olp_network_HttpClient_dataCallback(JNIEnv* env, jobject obj, * Callback to be called when a request is completed */ extern "C" OLP_SDK_NETWORK_ANDROID_EXPORT void JNICALL -Java_com_here_olp_network_HttpClient_completeRequest( +Java_com_here_olp_network_OlpHttpClient_completeRequest( JNIEnv* env, jobject obj, jlong request_id, jint status, jint uploaded_bytes, jint downloaded_bytes, jstring error, jstring content_type) { @@ -279,7 +281,7 @@ bool NetworkAndroid::Initialize() { // Get corresponding HttpClient class jstring network_class_name = - env->NewStringUTF("com/here/olp/network/HttpClient"); + env->NewStringUTF("com/here/olp/network/OlpHttpClient"); if (env->ExceptionOccurred()) { OLP_SDK_LOG_ERROR( kLogTag, @@ -338,10 +340,10 @@ bool NetworkAndroid::Initialize() { env->DeleteLocalRef(obj); // Get send method - jni_send_method_ = - env->GetMethodID(java_self_class_, "send", - "(Ljava/lang/String;IJII[Ljava/lang/String;[BLjava/lang/" - "String;II)Lcom/here/olp/network/HttpClient$HttpTask;"); + jni_send_method_ = env->GetMethodID( + java_self_class_, "send", + "(Ljava/lang/String;IJII[Ljava/lang/String;[BLjava/lang/" + "String;II)Lcom/here/olp/network/OlpHttpClient$HttpTask;"); if (env->ExceptionOccurred()) { OLP_SDK_LOG_ERROR( @@ -371,16 +373,16 @@ bool NetworkAndroid::Initialize() { JNINativeMethod methods[] = { {"headersCallback", "(J[Ljava/lang/String;)V", reinterpret_cast( - &Java_com_here_olp_network_HttpClient_headersCallback)}, + &Java_com_here_olp_network_OlpHttpClient_headersCallback)}, {"dateAndOffsetCallback", "(JJJ)V", reinterpret_cast( - &Java_com_here_olp_network_HttpClient_dateAndOffsetCallback)}, + &Java_com_here_olp_network_OlpHttpClient_dateAndOffsetCallback)}, {"dataCallback", "(J[BI)V", reinterpret_cast( - &Java_com_here_olp_network_HttpClient_dataCallback)}, + &Java_com_here_olp_network_OlpHttpClient_dataCallback)}, {"completeRequest", "(JIIILjava/lang/String;Ljava/lang/String;)V", reinterpret_cast( - &Java_com_here_olp_network_HttpClient_completeRequest)}}; + &Java_com_here_olp_network_OlpHttpClient_completeRequest)}}; env->RegisterNatives(java_self_class_, methods, sizeof(methods) / sizeof(methods[0])); diff --git a/olp-cpp-sdk-core/src/http/android/OlpHttpClient.java b/olp-cpp-sdk-core/src/http/android/OlpHttpClient.java new file mode 100644 index 000000000..4295ee70b --- /dev/null +++ b/olp-cpp-sdk-core/src/http/android/OlpHttpClient.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2019-2025 HERE Europe B.V. + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package com.here.olp.network; + +import android.os.AsyncTask; +import android.os.OperationCanceledException; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class OlpHttpClient { + // The replica of http/NetworkType.h error codes + public static final int IO_ERROR = -1; + public static final int AUTHORIZATION_ERROR = -2; + public static final int INVALID_URL_ERROR = -3; + public static final int OFFLINE_ERROR = -4; + public static final int CANCELLED_ERROR = -5; + public static final int TIMEOUT_ERROR = -7; + public static final int THREAD_POOL_SIZE = 8; + private static final String LOGTAG = "OlpHttpClient"; + // The raw pointer to the C++ NetworkAndroid class + private long nativePtr; + private ExecutorService executor; + + public OlpHttpClient() { + this.executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE); + } + + public static HttpVerb toHttpVerb(int verb) { + switch (verb) { + case 0: + return HttpVerb.GET; + case 1: + return HttpVerb.POST; + case 2: + return HttpVerb.HEAD; + case 3: + return HttpVerb.PUT; + case 4: + return HttpVerb.DELETE; + case 5: + return HttpVerb.PATCH; + case 6: + return HttpVerb.OPTIONS; + default: + return HttpVerb.GET; + } + } + + public void shutdown() { + if (this.executor != null) { + this.executor.shutdown(); + this.executor = null; + } + synchronized (this) { + // deinitialize the nativePtr to stop receiving scheduled events from Java to C++ + this.nativePtr = 0; + } + } + + public HttpTask send( + String url, + int httpMethod, + long requestId, + int connTimeout, + int reqTimeout, + String[] headers, + byte[] postData, + String proxyServer, + int proxyPort, + int proxyType) { + final OlpRequest request = + new OlpRequest( + url, + OlpHttpClient.toHttpVerb(httpMethod), + requestId, + connTimeout, + reqTimeout, + headers, + postData, + proxyServer, + proxyPort, + proxyType); + final HttpTask task = new HttpTask(this); + task.executeOnExecutor(executor, request); + return task; + } + + // Native methods: + // Synchronization is required in order to provide thread-safe access to `nativePtr` + // Callback for completed request + private synchronized native void completeRequest( + long requestId, int status, int uploadedBytes, int downloadedBytes, String error, String contentType); + + // Callback for data received + private synchronized native void dataCallback(long requestId, byte[] data, int len); + + // Callback set date and offset + private synchronized native void dateAndOffsetCallback(long requestId, long date, long offset); + + // Callback set date and offset + private synchronized native void headersCallback(long requestId, String[] headers); + + public enum HttpVerb { + GET, + POST, + HEAD, + PUT, + DELETE, + PATCH, + OPTIONS + } + + /** + * Task class sends the request HttpUrlConnection and responsible for handling response as well. + */ + private static class HttpTask extends AsyncTask { + private final WeakReference weakReference; + private final AtomicBoolean cancelled = new AtomicBoolean(false); + + private HttpTask(OlpHttpClient client) { + weakReference = new WeakReference<>(client); + } + + public synchronized void cancelTask() { + this.cancelled.set(true); + } + + private Proxy createProxy(OlpRequest olpRequest) { + if (olpRequest.noProxy()) { + return Proxy.NO_PROXY; + } + + return new Proxy( + olpRequest.proxyType(), + new InetSocketAddress(olpRequest.proxyServer(), olpRequest.proxyPort())); + } + + @Override + protected Void doInBackground(OlpRequest... olpRequests) { + for (OlpRequest olpRequest : olpRequests) { + // TODO: calculate downloaded and uploaded bytes + // TODO: check cancellation + // TODO: handle exceptions + + final OlpHttpClient olpHttpClient = weakReference.get(); + if (olpHttpClient == null) { + return null; + } + + OkHttpClient client = new OkHttpClient.Builder() + .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)) + .proxy(createProxy(olpRequest)) + .connectTimeout(olpRequest.connectTimeout(), TimeUnit.SECONDS) + .readTimeout(olpRequest.requestTimeout(), TimeUnit.SECONDS) + .build(); + + try { + Request.Builder requestBuilder = new Request.Builder(); + requestBuilder.url(olpRequest.url()); + + if (olpRequest.postData() != null) { + requestBuilder.method(olpRequest.verb().toString(), RequestBody.create(olpRequest.postData())); + } else { + requestBuilder.method(olpRequest.verb().toString(), null); + } + + { + String[] headers = olpRequest.headers(); + if (headers != null) { + Headers.Builder headersBuilder = new Headers.Builder(); + + for (int j = 0; (j + 1) < headers.length; j += 2) { + headersBuilder.add(headers[j], headers[j + 1]); + } + + requestBuilder.headers(headersBuilder.build()); + } + } + + Request request = requestBuilder.build(); + + Log.i(LOGTAG, "Request: " + request); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + + Log.i(LOGTAG, "Response: " + response); + + try (InputStream inputStream = Objects.requireNonNull(response.body()).byteStream()) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + olpHttpClient.dataCallback(olpRequest.requestId(), buffer, bytesRead); + } + } catch (IOException e) { + e.printStackTrace(); + } + + String contentType = response.header("Content-Type", ""); + olpHttpClient.completeRequest(olpRequest.requestId(), response.code(), 0, 0, response.message(), contentType); + } catch (IOException e) { + e.printStackTrace(); + } + + + } catch (Exception e) { + e.printStackTrace(); + } + } + + return null; + } + + private void completeErrorRequest(long requestId, int status, int uploadedBytes, int downloadedBytes, String error) { + final OlpHttpClient olpHttpClient = weakReference.get(); + if (olpHttpClient != null) { + olpHttpClient.completeRequest(requestId, status, uploadedBytes, downloadedBytes, error, ""); + } + } + + private void checkCancelled() { + if (this.cancelled.get()) { + throw new OperationCanceledException(); + } + } + + private int calculateHeadersSize(Map> headers) throws IOException { + int size = 0; + for (Map.Entry> entry : headers.entrySet()) { + String header = entry.getKey(); + List values = entry.getValue(); + if (header != null) { + size += header.length(); + } + for (String value : values) { + if (value != null) { + size += value.length(); + } + } + } + return size; + } + } + + /** + * Class to hold the request's data, which will be used by HttpUrlConnection object. + */ + public final class OlpRequest { + private final String url; + private final long requestId; + private final int connectionTimeout; + private final int requestTimeout; + private final String[] headers; + private final byte[] postData; + private final HttpVerb verb; + private final String proxyServer; + private final int proxyPort; + private final Proxy.Type proxyType; + + public OlpRequest( + String url, + HttpVerb verb, + long requestId, + int connectionTimeout, + int requestTimeout, + String[] headers, + byte[] postData, + String proxyServer, + int proxyPort, + int proxyType) { + this.url = url; + this.verb = verb; + this.requestId = requestId; + this.connectionTimeout = connectionTimeout; + this.requestTimeout = requestTimeout; + this.headers = headers; + this.postData = postData; + this.proxyServer = proxyServer; + this.proxyPort = proxyPort; + + switch (proxyType) { + case 0: + this.proxyType = Proxy.Type.DIRECT; + break; + case 1: + this.proxyType = Proxy.Type.HTTP; + break; + case 2: + Log.w(LOGTAG, "OlpHttpClient::Request(): Unsupported proxy version (" + proxyType + "). Falling back to HTTP(1)"); + this.proxyType = Proxy.Type.HTTP; + break; + case 3: + this.proxyType = Proxy.Type.SOCKS; + break; + case 4: + case 5: + case 6: + Log.w(LOGTAG, "OlpHttpClient::Request(): Unsupported proxy version (" + proxyType + "). Falling back to SOCKS4(3)"); + this.proxyType = Proxy.Type.SOCKS; + break; + default: + Log.w(LOGTAG, "OlpHttpClient::Request(): Unsupported proxy version (" + proxyType + "). Falling back to HTTP(1)"); + this.proxyType = Proxy.Type.HTTP; + break; + } + } + + public String url() { + return this.url; + } + + public HttpVerb verb() { + return this.verb; + } + + public long requestId() { + return this.requestId; + } + + public int connectTimeout() { + return this.connectionTimeout; + } + + public int requestTimeout() { + return this.requestTimeout; + } + + public String[] headers() { + return this.headers; + } + + public byte[] postData() { + return this.postData; + } + + public String proxyServer() { + return this.proxyServer; + } + + public int proxyPort() { + return this.proxyPort; + } + + public Proxy.Type proxyType() { + return this.proxyType; + } + + public boolean hasProxy() { + return (this.proxyServer != null) && !this.proxyServer.isEmpty(); + } + + public boolean noProxy() { + return (hasProxy() && this.proxyServer.equals("No")) || this.proxyType == Proxy.Type.DIRECT; + } + } +}