diff options
author | Leith Bade <leith@mapbox.com> | 2015-07-31 13:00:52 +1000 |
---|---|---|
committer | Leith Bade <leith@mapbox.com> | 2015-08-11 11:59:08 +1000 |
commit | 0cbe7e07f5ada444de45b3936f7378dd4f3b9d6b (patch) | |
tree | 72e2e56f37659d9b55b2815ed9aa8a1e363c1184 | |
parent | dc11b8132ba7ccc58c280e580526425e7389df86 (diff) | |
download | qtlocation-mapboxgl-0cbe7e07f5ada444de45b3936f7378dd4f3b9d6b.tar.gz |
Use OkHTTP to implement HTTPContext on Android.
Closes #823
-rw-r--r-- | android/cpp/jni.cpp | 142 | ||||
-rw-r--r-- | android/cpp/native_map_view.cpp | 81 | ||||
-rw-r--r-- | android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/http/HTTPContext.java | 93 | ||||
-rw-r--r-- | android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/util/JavaFileSource.java | 51 | ||||
-rw-r--r-- | android/mapboxgl-app.gypi | 5 | ||||
-rw-r--r-- | gyp/http-android.gypi | 70 | ||||
-rw-r--r-- | include/mbgl/android/jni.hpp | 29 | ||||
-rw-r--r-- | mbgl.gyp | 1 | ||||
-rw-r--r-- | platform/android/http_request_android.cpp | 399 | ||||
-rw-r--r-- | platform/default/http_request_curl.cpp | 109 | ||||
-rw-r--r-- | scripts/android/configure.sh | 4 | ||||
-rw-r--r-- | scripts/android/defaults.mk | 2 |
12 files changed, 734 insertions, 252 deletions
diff --git a/android/cpp/jni.cpp b/android/cpp/jni.cpp index 6d7def861c..f1e9072cbd 100644 --- a/android/cpp/jni.cpp +++ b/android/cpp/jni.cpp @@ -28,6 +28,8 @@ namespace mbgl { namespace android { +JavaVM* theJVM; + std::string cachePath; std::string dataPath; std::string apkPath; @@ -90,7 +92,15 @@ jmethodID pointFConstructorId = nullptr; jfieldID pointFXId = nullptr; jfieldID pointFYId = nullptr; -bool throw_error(JNIEnv *env, const char *msg) { +jclass httpContextClass = nullptr; +jmethodID httpContextGetInstanceId = nullptr; +jmethodID httpContextCreateRequestId = nullptr; + +jclass httpRequestClass = nullptr; +jmethodID httpRequestStartId = nullptr; +jmethodID httpRequestCancelId = nullptr; + +bool throw_jni_error(JNIEnv *env, const char *msg) { if (env->ThrowNew(runtimeExceptionClass, msg) < 0) { env->ExceptionDescribe(); return false; @@ -99,6 +109,41 @@ bool throw_error(JNIEnv *env, const char *msg) { return true; } +bool attach_jni_thread(JavaVM* vm, JNIEnv** env, std::string threadName) { + JavaVMAttachArgs args = {JNI_VERSION_1_2, threadName.c_str(), NULL}; + + jint ret; + *env = nullptr; + bool detach = false; + ret = vm->GetEnv(reinterpret_cast<void **>(env), JNI_VERSION_1_6); + if (ret != JNI_OK) { + if (ret != JNI_EDETACHED) { + mbgl::Log::Error(mbgl::Event::JNI, "GetEnv() failed with %i", ret); + throw new std::runtime_error("GetEnv() failed"); + } else { + ret = vm->AttachCurrentThread(env, &args); + if (ret != JNI_OK) { + mbgl::Log::Error(mbgl::Event::JNI, "AttachCurrentThread() failed with %i", ret); + throw new std::runtime_error("AttachCurrentThread() failed"); + } + detach = true; + } + } + + return detach; +} + +void detach_jni_thread(JavaVM* vm, JNIEnv** env, bool detach) { + if (detach) { + jint ret; + if ((ret = vm->DetachCurrentThread()) != JNI_OK) { + mbgl::Log::Error(mbgl::Event::JNI, "DetachCurrentThread() failed with %i", ret); + throw new std::runtime_error("DetachCurrentThread() failed"); + } + } + *env = nullptr; +} + std::string std_string_from_jstring(JNIEnv *env, jstring jstr) { std::string str; @@ -342,7 +387,7 @@ void JNICALL nativeInitializeDisplay(JNIEnv *env, jobject obj, jlong nativeMapVi { nativeMapView->initializeDisplay(); } catch(const std::exception& e) { - throw_error(env, "Unable to initialize GL display."); + throw_jni_error(env, "Unable to initialize GL display."); } } @@ -361,7 +406,7 @@ void JNICALL nativeInitializeContext(JNIEnv *env, jobject obj, jlong nativeMapVi try { nativeMapView->initializeContext(); } catch(const std::exception& e) { - throw_error(env, "Unable to initialize GL context."); + throw_jni_error(env, "Unable to initialize GL context."); } } @@ -380,7 +425,7 @@ void JNICALL nativeCreateSurface(JNIEnv *env, jobject obj, jlong nativeMapViewPt try { nativeMapView->createSurface(ANativeWindow_fromSurface(env, surface)); } catch(const std::exception& e) { - throw_error(env, "Unable to create GL surface."); + throw_jni_error(env, "Unable to create GL surface."); } } @@ -1131,6 +1176,8 @@ extern "C" { extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { mbgl::Log::Debug(mbgl::Event::JNI, "JNI_OnLoad"); + theJVM = vm; + JNIEnv *env = nullptr; jint ret = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); if (ret != JNI_OK) { @@ -1426,6 +1473,36 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_ERR; } + httpContextClass = env->FindClass("com/mapbox/mapboxgl/http/HTTPContext"); + if (httpContextClass == nullptr) { + env->ExceptionDescribe(); + } + + httpContextGetInstanceId = env->GetStaticMethodID(httpContextClass, "getInstance", "()Lcom/mapbox/mapboxgl/http/HTTPContext;"); + if (httpContextGetInstanceId == nullptr) { + env->ExceptionDescribe(); + } + + httpContextCreateRequestId = env->GetMethodID(httpContextClass, "createRequest", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/mapbox/mapboxgl/http/HTTPContext$HTTPRequest;"); + if (httpContextCreateRequestId == nullptr) { + env->ExceptionDescribe(); + } + + httpRequestClass = env->FindClass("com/mapbox/mapboxgl/http/HTTPContext$HTTPRequest"); + if (httpRequestClass == nullptr) { + env->ExceptionDescribe(); + } + + httpRequestStartId = env->GetMethodID(httpRequestClass, "start", "()V"); + if (httpRequestStartId == nullptr) { + env->ExceptionDescribe(); + } + + httpRequestCancelId = env->GetMethodID(httpRequestClass, "cancel", "()V"); + if (httpRequestCancelId == nullptr) { + env->ExceptionDescribe(); + } + const std::vector<JNINativeMethod> methods = { {"nativeCreate", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;FIJ)J", reinterpret_cast<void *>(&nativeCreate)}, @@ -1524,10 +1601,16 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { //{"nativeGetWorldBoundsMeters", "(J)V", reinterpret_cast<void *>(&nativeGetWorldBoundsMeters)}, //{"nativeGetWorldBoundsLatLng", "(J)V", reinterpret_cast<void *>(&nativeGetWorldBoundsLatLng)}, {"nativeGetMetersPerPixelAtLatitude", "(JDD)D", reinterpret_cast<void *>(&nativeGetMetersPerPixelAtLatitude)}, - {"nativeProjectedMetersForLatLng", "(JLcom/mapbox/mapboxgl/geometry/LatLng;)Lcom/mapbox/mapboxgl/geometry/ProjectedMeters;", reinterpret_cast<void *>(&nativeProjectedMetersForLatLng)}, - {"nativeLatLngForProjectedMeters", "(JLcom/mapbox/mapboxgl/geometry/ProjectedMeters;)Lcom/mapbox/mapboxgl/geometry/LatLng;", reinterpret_cast<void *>(&nativeLatLngForProjectedMeters)}, - {"nativePixelForLatLng", "(JLcom/mapbox/mapboxgl/geometry/LatLng;)Landroid/graphics/PointF;", reinterpret_cast<void *>(&nativePixelForLatLng)}, - {"nativeLatLngForPixel", "(JLandroid/graphics/PointF;)Lcom/mapbox/mapboxgl/geometry/LatLng;", reinterpret_cast<void *>(&nativeLatLngForPixel)}, + {"nativeProjectedMetersForLatLng", + "(JLcom/mapbox/mapboxgl/geometry/LatLng;)Lcom/mapbox/mapboxgl/geometry/ProjectedMeters;", + reinterpret_cast<void *>(&nativeProjectedMetersForLatLng)}, + {"nativeLatLngForProjectedMeters", + "(JLcom/mapbox/mapboxgl/geometry/ProjectedMeters;)Lcom/mapbox/mapboxgl/geometry/LatLng;", + reinterpret_cast<void *>(&nativeLatLngForProjectedMeters)}, + {"nativePixelForLatLng", "(JLcom/mapbox/mapboxgl/geometry/LatLng;)Landroid/graphics/PointF;", + reinterpret_cast<void *>(&nativePixelForLatLng)}, + {"nativeLatLngForPixel", "(JLandroid/graphics/PointF;)Lcom/mapbox/mapboxgl/geometry/LatLng;", + reinterpret_cast<void *>(&nativeLatLngForPixel)}, }; if (env->RegisterNatives(nativeMapViewClass, methods.data(), methods.size()) < 0) { @@ -1641,6 +1724,37 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_ERR; } + httpContextClass = reinterpret_cast<jclass>(env->NewGlobalRef(httpContextClass)); + if (httpContextClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(latLngZoomClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + } + + httpRequestClass = reinterpret_cast<jclass>(env->NewGlobalRef(httpRequestClass)); + if (httpRequestClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(latLngZoomClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(httpContextClass); + } + char release[PROP_VALUE_MAX] = ""; __system_property_get("ro.build.version.release", release); androidRelease = std::string(release); @@ -1651,6 +1765,8 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { extern "C" JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) { mbgl::Log::Debug(mbgl::Event::JNI, "JNI_OnUnload"); + theJVM = vm; + JNIEnv *env = nullptr; jint ret = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); if (ret != JNI_OK) { @@ -1725,5 +1841,15 @@ extern "C" JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) { pointFConstructorId = nullptr; pointFXId = nullptr; pointFYId = nullptr; + + env->DeleteGlobalRef(httpContextClass); + httpContextGetInstanceId = nullptr; + httpContextCreateRequestId = nullptr; + + env->DeleteGlobalRef(httpRequestClass); + httpRequestStartId = nullptr; + httpRequestCancelId = nullptr; + + theJVM = nullptr; } } diff --git a/android/cpp/native_map_view.cpp b/android/cpp/native_map_view.cpp index d0581212df..d7c40ba5a1 100644 --- a/android/cpp/native_map_view.cpp +++ b/android/cpp/native_map_view.cpp @@ -148,38 +148,15 @@ void NativeMapView::invalidate() { assert(vm != nullptr); assert(obj != nullptr); - JavaVMAttachArgs args = {JNI_VERSION_1_2, "NativeMapView::invalidate()", NULL}; - - jint ret; JNIEnv *env = nullptr; - bool detach = false; - ret = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); - if (ret != JNI_OK) { - if (ret != JNI_EDETACHED) { - mbgl::Log::Error(mbgl::Event::JNI, "GetEnv() failed with %i", ret); - throw new std::runtime_error("GetEnv() failed"); - } else { - ret = vm->AttachCurrentThread(&env, &args); - if (ret != JNI_OK) { - mbgl::Log::Error(mbgl::Event::JNI, "AttachCurrentThread() failed with %i", ret); - throw new std::runtime_error("AttachCurrentThread() failed"); - } - detach = true; - } - } + bool detach = attach_jni_thread(vm, &env, "NativeMapView::invalidate()"); env->CallVoidMethod(obj, onInvalidateId); if (env->ExceptionCheck()) { env->ExceptionDescribe(); } - if (detach) { - if ((ret = vm->DetachCurrentThread()) != JNI_OK) { - mbgl::Log::Error(mbgl::Event::JNI, "DetachCurrentThread() failed with %i", ret); - throw new std::runtime_error("DetachCurrentThread() failed"); - } - } - env = nullptr; + detach_jni_thread(vm, &env, detach); } void NativeMapView::swap() { @@ -662,38 +639,15 @@ void NativeMapView::notifyMapChange(mbgl::MapChange) { assert(vm != nullptr); assert(obj != nullptr); - JavaVMAttachArgs args = {JNI_VERSION_1_2, "NativeMapView::notifyMapChange()", NULL}; - - jint ret; JNIEnv *env = nullptr; - bool detach = false; - ret = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); - if (ret != JNI_OK) { - if (ret != JNI_EDETACHED) { - mbgl::Log::Error(mbgl::Event::JNI, "GetEnv() failed with %i", ret); - throw new std::runtime_error("GetEnv() failed"); - } else { - ret = vm->AttachCurrentThread(&env, &args); - if (ret != JNI_OK) { - mbgl::Log::Error(mbgl::Event::JNI, "AttachCurrentThread() failed with %i", ret); - throw new std::runtime_error("AttachCurrentThread() failed"); - } - detach = true; - } - } + bool detach = attach_jni_thread(vm, &env, "NativeMapView::notifyMapChange()"); env->CallVoidMethod(obj, onMapChangedId); if (env->ExceptionCheck()) { env->ExceptionDescribe(); } - if (detach) { - if ((ret = vm->DetachCurrentThread()) != JNI_OK) { - mbgl::Log::Error(mbgl::Event::JNI, "DetachCurrentThread() failed with %i", ret); - throw new std::runtime_error("DetachCurrentThread() failed"); - } - } - env = nullptr; + detach_jni_thread(vm, &env, detach); } void NativeMapView::enableFps(bool enable) { @@ -727,38 +681,15 @@ void NativeMapView::updateFps() { assert(vm != nullptr); assert(obj != nullptr); - JavaVMAttachArgs args = {JNI_VERSION_1_2, "NativeMapView::updateFps()", NULL}; - - jint ret; JNIEnv *env = nullptr; - bool detach = false; - ret = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); - if (ret != JNI_OK) { - if (ret != JNI_EDETACHED) { - mbgl::Log::Error(mbgl::Event::JNI, "GetEnv() failed with %i", ret); - throw new std::runtime_error("GetEnv() failed"); - } else { - ret = vm->AttachCurrentThread(&env, &args); - if (ret != JNI_OK) { - mbgl::Log::Error(mbgl::Event::JNI, "AttachCurrentThread() failed with %i", ret); - throw new std::runtime_error("AttachCurrentThread() failed"); - } - detach = true; - } - } + bool detach = attach_jni_thread(vm, &env, "NativeMapView::updateFps()"); env->CallVoidMethod(obj, onFpsChangedId, fps); if (env->ExceptionCheck()) { env->ExceptionDescribe(); } - if (detach) { - if ((ret = vm->DetachCurrentThread()) != JNI_OK) { - mbgl::Log::Error(mbgl::Event::JNI, "DetachCurrentThread() failed with %i", ret); - throw new std::runtime_error("DetachCurrentThread() failed"); - } - } - env = nullptr; + detach_jni_thread(vm, &env, detach); } void NativeMapView::onInvalidate() { diff --git a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/http/HTTPContext.java b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/http/HTTPContext.java new file mode 100644 index 0000000000..509c79a4a5 --- /dev/null +++ b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/http/HTTPContext.java @@ -0,0 +1,93 @@ +package com.mapbox.mapboxgl.http; + +import com.mapbox.mapboxgl.constants.MapboxConstants; +import com.squareup.okhttp.Call; +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.ProtocolException; +import java.net.SocketException; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLException; + +class HTTPContext { + + private static final int CONNECTION_ERROR = 0; + private static final int TEMPORARY_ERROR = 1; + private static final int PERMANENT_ERROR = 2; + + private static HTTPContext mInstance = null; + + private OkHttpClient mClient; + + private HTTPContext() { + super(); + mClient = new OkHttpClient(); + } + + public static HTTPContext getInstance() { + if (mInstance == null) { + mInstance = new HTTPContext(); + } + + return mInstance; + } + + public HTTPRequest createRequest(long nativePtr, String resourceUrl, String userAgent, String etag, String modified) { + return new HTTPRequest(nativePtr, resourceUrl, userAgent, etag, modified); + } + + public class HTTPRequest implements Callback { + private long mNativePtr = 0; + + private Call mCall; + private Request mRequest; + + private native void nativeOnFailure(long nativePtr, int type, String message); + private native void nativeOnResponse(long nativePtr, int code, String message, String etag, String modified, String cacheControl, String expires, byte[] body); + + private HTTPRequest(long nativePtr, String resourceUrl, String userAgent, String etag, String modified) { + mNativePtr = nativePtr; + Request.Builder builder = new Request.Builder().url(resourceUrl).tag(resourceUrl.toLowerCase(MapboxConstants.MAPBOX_LOCALE)).addHeader("User-Agent", userAgent); + if (etag.length() > 0) { + builder = builder.addHeader("If-None-Match", etag); + } else if (modified.length() > 0) { + builder = builder.addHeader("If-Modified-Since", modified); + } + mRequest = builder.build(); + } + + public void start() { + mCall = HTTPContext.getInstance().mClient.newCall(mRequest); + mCall.enqueue(this); + } + + public void cancel() { + mCall.cancel(); + } + + @Override + public void onFailure(Request request, IOException e) { + int type = PERMANENT_ERROR; + if ((e instanceof UnknownHostException) || (e instanceof SocketException) || (e instanceof ProtocolException) || (e instanceof SSLException)) { + type = CONNECTION_ERROR; + } else if ((e instanceof InterruptedIOException)) { + type = TEMPORARY_ERROR; + } + nativeOnFailure(mNativePtr, type, e.getMessage()); + } + + @Override + public void onResponse(Response response) throws IOException { + byte[] body = response.body().bytes(); + response.body().close(); + + nativeOnResponse(mNativePtr, response.code(), response.message(), response.header("ETag"), response.header("Last-Modified"), response.header("Cache-Control"), response.header("Expires"), body); + } + } +} diff --git a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/util/JavaFileSource.java b/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/util/JavaFileSource.java deleted file mode 100644 index bd86241888..0000000000 --- a/android/java/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxgl/util/JavaFileSource.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.mapbox.mapboxgl.util; - -import com.mapbox.mapboxgl.constants.MapboxConstants; -import com.squareup.okhttp.Callback; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; - -public class JavaFileSource { - - private static JavaFileSource instance = null; - - // Single reference to OkHttp for performance gains - private OkHttpClient client; - - /** - * Private Constructor to support Singleton pattern - */ - private JavaFileSource() { - super(); - client = new OkHttpClient(); - } - - /** - * Get the singleton instance of JavaFileSource - * @return Reference to the Singleton Instance of JavaFileSource - */ - public static JavaFileSource getInstance() { - if (instance == null) { - instance = new JavaFileSource(); - } - return instance; - } - - /** - * Make an HTTP Request - * @param resourceUrl URL to resource - * @param callback Callback class - */ - public void request(final String resourceUrl, final Callback callback) { - Request request = new Request.Builder().url(resourceUrl).tag(resourceUrl.toLowerCase(MapboxConstants.MAPBOX_LOCALE)).build(); - client.newCall(request).enqueue(callback); - } - - /** - * Attempt to cancel HTTP Request made - * @param resourceUrl URL of request to cancel - */ - public void cancel(final String resourceUrl) { - client.cancel(resourceUrl.toLowerCase(MapboxConstants.MAPBOX_LOCALE)); - } -} diff --git a/android/mapboxgl-app.gypi b/android/mapboxgl-app.gypi index e32775ff72..e3db1269d6 100644 --- a/android/mapboxgl-app.gypi +++ b/android/mapboxgl-app.gypi @@ -25,8 +25,6 @@ '<@(boost_cflags)', ], 'libraries': [ - '<@(openssl_static_libs)', - '<@(libcurl_static_libs)', '<@(libpng_static_libs)', '<@(jpeg_static_libs)', '<@(sqlite_static_libs)', @@ -45,8 +43,6 @@ '<@(libpng_ldflags)', '<@(jpeg_ldflags)', '<@(sqlite_ldflags)', - '<@(openssl_ldflags)', - '<@(libcurl_ldflags)', '<@(zlib_ldflags)', '<@(libzip_ldflags)', ], @@ -74,7 +70,6 @@ 'copies': [ { 'files': [ - '../common/ca-bundle.crt', '../styles/styles' ], 'destination': '<(pwd)/../android/java/MapboxGLAndroidSDK/src/main/assets' diff --git a/gyp/http-android.gypi b/gyp/http-android.gypi new file mode 100644 index 0000000000..c210db0fb6 --- /dev/null +++ b/gyp/http-android.gypi @@ -0,0 +1,70 @@ +{ + 'targets': [ + { 'target_name': 'http-android', + 'product_name': 'mbgl-http-android', + 'type': 'static_library', + 'standalone_static_library': 1, + 'hard_dependency': 1, + + 'sources': [ + '../platform/android/http_request_android.cpp', + ], + + 'include_dirs': [ + '../include', + '../src', + ], + + 'variables': { + 'cflags_cc': [ + '<@(libuv_cflags)', + '<@(boost_cflags)', + ], + 'ldflags': [ + '<@(libuv_ldflags)', + ], + 'libraries': [ + '<@(libuv_static_libs)', + ], + 'defines': [ + '-DMBGL_HTTP_ANDROID' + ], + }, + + 'conditions': [ + ['OS == "mac"', { + 'xcode_settings': { + 'OTHER_CPLUSPLUSFLAGS': [ '<@(cflags_cc)' ], + }, + }, { + 'cflags_cc': [ '<@(cflags_cc)' ], + }], + ], + + 'direct_dependent_settings': { + 'conditions': [ + ['OS == "mac"', { + 'xcode_settings': { + 'OTHER_CFLAGS': [ '<@(defines)' ], + 'OTHER_CPLUSPLUSFLAGS': [ '<@(defines)' ], + } + }, { + 'cflags': [ '<@(defines)' ], + 'cflags_cc': [ '<@(defines)' ], + }] + ], + }, + + 'link_settings': { + 'conditions': [ + ['OS == "mac"', { + 'libraries': [ '<@(libraries)' ], + 'xcode_settings': { 'OTHER_LDFLAGS': [ '<@(ldflags)' ] } + }, { + 'libraries': [ '<@(libraries)', '<@(ldflags)' ], + }] + ], + }, + }, + ], +} diff --git a/include/mbgl/android/jni.hpp b/include/mbgl/android/jni.hpp index f27c8c8449..62cb399d3f 100644 --- a/include/mbgl/android/jni.hpp +++ b/include/mbgl/android/jni.hpp @@ -2,15 +2,27 @@ #define MBGL_ANDROID_JNI #include <string> +#include <vector> // Forward definition of JNI types typedef class _jclass* jclass; +typedef class _jstring* jstring; +typedef class _jobject* jobject; +typedef class _jlongArray* jlongArray; typedef struct _jmethodID* jmethodID; typedef struct _jfieldID* jfieldID; +struct _JavaVM; +typedef _JavaVM JavaVM; + +struct _JNIEnv; +typedef _JNIEnv JNIEnv; + namespace mbgl { namespace android { +extern JavaVM* theJVM; + extern std::string cachePath; extern std::string dataPath; extern std::string apkPath; @@ -73,6 +85,23 @@ extern jmethodID pointFConstructorId; extern jfieldID pointFXId; extern jfieldID pointFYId; +extern jclass httpContextClass; +extern jmethodID httpContextGetInstanceId; +extern jmethodID httpContextCreateRequestId; + +extern jclass httpRequestClass; +extern jmethodID httpRequestStartId; +extern jmethodID httpRequestCancelId; + +extern bool throw_jni_error(JNIEnv *env, const char *msg); +extern bool attach_jni_thread(JavaVM* vm, JNIEnv** env, std::string threadName); +extern void detach_jni_thread(JavaVM* vm, JNIEnv** env, bool detach); +extern std::string std_string_from_jstring(JNIEnv *env, jstring jstr); +extern jstring std_string_to_jstring(JNIEnv *env, std::string str); +extern std::vector<std::string> std_vector_string_from_jobject(JNIEnv *env, jobject jlist); +extern jobject std_vector_string_to_jobject(JNIEnv *env, std::vector<std::string> vector); +extern jlongArray std_vector_uint_to_jobject(JNIEnv *env, std::vector<uint32_t> vector); + } } @@ -18,6 +18,7 @@ ['platform_lib == "android" and host == "android"', { 'includes': [ './gyp/platform-android.gypi' ] } ], ['http_lib == "curl"', { 'includes': [ './gyp/http-curl.gypi' ] } ], ['http_lib == "nsurl" and (host == "osx" or host == "ios")', { 'includes': [ './gyp/http-nsurl.gypi' ] } ], + ['http_lib == "android" and host == "android"', { 'includes': [ './gyp/http-android.gypi' ] } ], ['asset_lib == "fs"', { 'includes': [ './gyp/asset-fs.gypi' ] } ], ['asset_lib == "zip"', { 'includes': [ './gyp/asset-zip.gypi' ] } ], ['cache_lib == "sqlite"', { 'includes': [ './gyp/cache-sqlite.gypi' ] } ], diff --git a/platform/android/http_request_android.cpp b/platform/android/http_request_android.cpp new file mode 100644 index 0000000000..044f772628 --- /dev/null +++ b/platform/android/http_request_android.cpp @@ -0,0 +1,399 @@ +#include <mbgl/storage/http_context_base.hpp> +#include <mbgl/storage/http_request_base.hpp> +#include <mbgl/storage/resource.hpp> +#include <mbgl/storage/response.hpp> +#include <mbgl/util/chrono.hpp> +#include <mbgl/platform/log.hpp> +#include <mbgl/android/jni.hpp> + +#include <mbgl/util/time.hpp> +#include <mbgl/util/util.hpp> +#include <mbgl/util/string.hpp> +#include <mbgl/util/parsedate.h> + +#include <jni.h> + +namespace mbgl { + +void JNICALL nativeOnFailure(JNIEnv *env, jobject obj, jlong nativePtr, jint type, jstring message); +void JNICALL nativeOnResponse(JNIEnv *env, jobject obj, jlong nativePtr, jint code, jstring message, jstring etag, jstring modified, jstring cacheControl, jstring expires, jbyteArray body); + +class HTTPAndroidRequest; + +class HTTPAndroidContext : public HTTPContextBase { +public: + explicit HTTPAndroidContext(uv_loop_t *loop); + ~HTTPAndroidContext(); + + HTTPRequestBase* createRequest(const Resource&, + RequestBase::Callback, + uv_loop_t*, + std::shared_ptr<const Response>) final; + + uv_loop_t *loop = nullptr; + + JavaVM *vm = nullptr; + jobject obj = nullptr; +}; + +class HTTPAndroidRequest : public HTTPRequestBase { +public: + HTTPAndroidRequest(HTTPAndroidContext*, + const Resource&, + Callback, + uv_loop_t*, + std::shared_ptr<const Response>); + ~HTTPAndroidRequest(); + + void cancel() final; + void retry() final; + + void onFailure(int type, std::string message); + void onResponse(int code, std::string message, std::string etag, std::string modified, std::string cacheControl, std::string expires, std::string body); + +private: + void retry(uint64_t timeout) final; +#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 + static void restart(uv_timer_t *timer, int); +#else + static void restart(uv_timer_t *timer); +#endif + void finish(ResponseStatus status); + void start(); + + HTTPAndroidContext *context = nullptr; + + bool cancelled = false; + + std::unique_ptr<Response> response; + const std::shared_ptr<const Response> existingResponse; + + jobject obj = nullptr; + + uv_timer_t *timer = nullptr; + enum : bool { PreemptImmediately, ExponentialBackoff } strategy = PreemptImmediately; + int attempts = 0; + + static const int maxAttempts = 4; + + static const int connectionError = 0; + static const int temporaryError = 1; + static const int permanentError = 1; +}; + +// ------------------------------------------------------------------------------------------------- + +HTTPAndroidContext::HTTPAndroidContext(uv_loop_t *loop_) + : HTTPContextBase(loop_), + loop(loop_), + vm(mbgl::android::theJVM) { + + JNIEnv *env = nullptr; + bool detach = mbgl::android::attach_jni_thread(vm, &env, "HTTPAndroidContext::HTTPAndroidContext()"); + + const std::vector<JNINativeMethod> methods = { + {"nativeOnFailure", "(JILjava/lang/String;)V", reinterpret_cast<void *>(&nativeOnFailure)}, + {"nativeOnResponse", + "(JILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B)V", + reinterpret_cast<void *>(&nativeOnResponse)} + }; + + if (env->RegisterNatives(mbgl::android::httpRequestClass, methods.data(), methods.size()) < 0) { + env->ExceptionDescribe(); + } + + obj = env->CallStaticObjectMethod(mbgl::android::httpContextClass, mbgl::android::httpContextGetInstanceId); + if (env->ExceptionCheck() || (obj == nullptr)) { + env->ExceptionDescribe(); + } + + obj = env->NewGlobalRef(obj); + if (obj == nullptr) { + env->ExceptionDescribe(); + } + + mbgl::android::detach_jni_thread(vm, &env, detach); +} + +HTTPAndroidContext::~HTTPAndroidContext() { + JNIEnv *env = nullptr; + bool detach = mbgl::android::attach_jni_thread(vm, &env, "HTTPAndroidContext::~HTTPAndroidContext()"); + + env->DeleteGlobalRef(obj); + obj = nullptr; + + mbgl::android::detach_jni_thread(vm, &env, detach); + + vm = nullptr; +} + +HTTPRequestBase* HTTPAndroidContext::createRequest(const Resource& resource, + RequestBase::Callback callback, + uv_loop_t* loop_, + std::shared_ptr<const Response> response) { + return new HTTPAndroidRequest(this, resource, callback, loop_, response); +} + +HTTPAndroidRequest::HTTPAndroidRequest(HTTPAndroidContext* context_, const Resource& resource_, Callback callback_, uv_loop_t*, std::shared_ptr<const Response> response_) + : HTTPRequestBase(resource_, callback_), + context(context_), + existingResponse(response_) { + + std::string etagStr; + std::string modifiedStr; + if (existingResponse) { + if (!existingResponse->etag.empty()) { + etagStr = existingResponse->etag; + } else if (existingResponse->modified) { + modifiedStr = util::rfc1123(existingResponse->modified); + } + } + + JNIEnv *env = nullptr; + bool detach = mbgl::android::attach_jni_thread(context->vm, &env, "HTTPAndroidContext::HTTPAndroidRequest()"); + + jstring resourceUrl = mbgl::android::std_string_to_jstring(env, resource.url); + jstring userAgent = mbgl::android::std_string_to_jstring(env, "MapboxGL/1.0"); + jstring etag = mbgl::android::std_string_to_jstring(env, etagStr); + jstring modified = mbgl::android::std_string_to_jstring(env, modifiedStr); + obj = env->CallObjectMethod(context->obj, mbgl::android::httpContextCreateRequestId, reinterpret_cast<jlong>(this), resourceUrl, userAgent, etag, modified); + if (env->ExceptionCheck() || (obj == nullptr)) { + env->ExceptionDescribe(); + } + + obj = env->NewGlobalRef(obj); + if (obj == nullptr) { + env->ExceptionDescribe(); + } + + mbgl::android::detach_jni_thread(context->vm, &env, detach); + + context->addRequest(this); + start(); +} + +HTTPAndroidRequest::~HTTPAndroidRequest() { + context->removeRequest(this); + + JNIEnv *env = nullptr; + bool detach = mbgl::android::attach_jni_thread(context->vm, &env, "HTTPAndroidContext::~HTTPAndroidRequest()"); + + env->DeleteGlobalRef(obj); + obj = nullptr; + + mbgl::android::detach_jni_thread(context->vm, &env, detach); + + if (timer) { + uv_timer_stop(timer); + uv::close(timer); + timer = nullptr; + } +} + +void HTTPAndroidRequest::cancel() { + cancelled = true; + + JNIEnv *env = nullptr; + bool detach = mbgl::android::attach_jni_thread(context->vm, &env, "HTTPAndroidContext::cancel()"); + + env->CallVoidMethod(obj, mbgl::android::httpRequestCancelId); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + } + + mbgl::android::detach_jni_thread(context->vm, &env, detach); +} + +void HTTPAndroidRequest::start() { + attempts++; + + JNIEnv *env = nullptr; + bool detach = mbgl::android::attach_jni_thread(context->vm, &env, "HTTPAndroidContext::start()"); + + env->CallVoidMethod(obj, mbgl::android::httpRequestStartId); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + } + + mbgl::android::detach_jni_thread(context->vm, &env, detach); +} + +void HTTPAndroidRequest::retry(uint64_t timeout) { + response.reset(); + + assert(!timer); + timer = new uv_timer_t; + timer->data = this; + uv_timer_init(context->loop, timer); + uv_timer_start(timer, restart, timeout, 0); +} + +void HTTPAndroidRequest::retry() { + if (timer && strategy == PreemptImmediately) { + uv_timer_stop(timer); + uv_timer_start(timer, restart, 0, 0); + } +} + +#if UV_VERSION_MAJOR == 0 && UV_VERSION_MINOR <= 10 +void HTTPAndroidRequest::restart(uv_timer_t *timer, int) { +#else +void HTTPAndroidRequest::restart(uv_timer_t *timer) { +#endif + auto baton = reinterpret_cast<HTTPAndroidRequest *>(timer->data); + + baton->timer = nullptr; + uv::close(timer); + + baton->start(); +} + +void HTTPAndroidRequest::finish(ResponseStatus status) { + if (status == ResponseStatus::TemporaryError && attempts < maxAttempts) { + strategy = ExponentialBackoff; + return retry((1 << (attempts - 1)) * 1000); + } else if (status == ResponseStatus::ConnectionError && attempts < maxAttempts) { + strategy = PreemptImmediately; + return retry(30000); + } + + if (status == ResponseStatus::NotModified) { + notify(std::move(response), FileCache::Hint::Refresh); + } else { + notify(std::move(response), FileCache::Hint::Full); + } + + delete this; +} + +int64_t parseCacheControl(const char *value) { + if (value) { + unsigned long long seconds = 0; + // TODO: cache-control may contain other information as well: + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 + if (std::sscanf(value, "max-age=%llu", &seconds) == 1) { + return std::chrono::duration_cast<std::chrono::seconds>( + std::chrono::system_clock::now().time_since_epoch()).count() + + seconds; + } + } + + return 0; +} + +void HTTPAndroidRequest::onResponse(int code, std::string message, std::string etag, std::string modified, std::string cacheControl, std::string expires, std::string body) { + if (cancelled) { + delete this; + return; + } + + if (!response) { + response = std::make_unique<Response>(); + } + + response->message = message; + response->modified = parse_date(modified.c_str()); + response->etag = etag; + response->expires = parseCacheControl(cacheControl.c_str()); + if (!expires.empty()) { + response->expires = parse_date(expires.c_str()); + } + response->data = body; + + if (code == 304) { + if (existingResponse) { + response->status = existingResponse->status; + response->message = existingResponse->message; + response->modified = existingResponse->modified; + response->etag = existingResponse->etag; + response->data = existingResponse->data; + return finish(ResponseStatus::NotModified); + } else { + response->status = Response::Successful; + return finish(ResponseStatus::Successful); + } + } else if (code == 200) { + response->status = Response::Successful; + return finish(ResponseStatus::Successful); + } else if (code >= 500 && code < 600) { + response->status = Response::Error; + response->message = "HTTP status code " + util::toString(code); + return finish(ResponseStatus::TemporaryError); + } else { + response->status = Response::Error; + response->message = "HTTP status code " + util::toString(code); + return finish(ResponseStatus::PermanentError); + } + + throw std::runtime_error("Response hasn't been handled"); +} + +void HTTPAndroidRequest::onFailure(int type, std::string message) { + if (cancelled) { + delete this; + return; + } + + if (!response) { + response = std::make_unique<Response>(); + } + + response->status = Response::Error; + response->message = message; + + switch (type) { + case connectionError: + return finish(ResponseStatus::ConnectionError); + + case temporaryError: + return finish(ResponseStatus::TemporaryError); + + default: + return finish(ResponseStatus::PermanentError); + } + + throw std::runtime_error("Response hasn't been handled"); +} + +std::unique_ptr<HTTPContextBase> HTTPContextBase::createContext(uv_loop_t* loop) { + return std::make_unique<HTTPAndroidContext>(loop); +} + +#pragma clang diagnostic ignored "-Wunused-parameter" + +void JNICALL nativeOnFailure(JNIEnv *env, jobject obj, jlong nativePtr, jint type, jstring message) { + mbgl::Log::Debug(mbgl::Event::JNI, "nativeOnFailure"); + assert(nativePtr != 0); + HTTPAndroidRequest *request = reinterpret_cast<HTTPAndroidRequest *>(nativePtr); + std::string messageStr = mbgl::android::std_string_from_jstring(env, message); + return request->onFailure(type, messageStr); +} + +void JNICALL nativeOnResponse(JNIEnv *env, jobject obj, jlong nativePtr, jint code, jstring message, jstring etag, jstring modified, jstring cacheControl, jstring expires, jbyteArray body) { + mbgl::Log::Debug(mbgl::Event::JNI, "nativeOnResponse"); + assert(nativePtr != 0); + HTTPAndroidRequest *request = reinterpret_cast<HTTPAndroidRequest *>(nativePtr); + std::string messageStr = mbgl::android::std_string_from_jstring(env, message); + std::string etagStr; + if (etag != nullptr) { + etagStr = mbgl::android::std_string_from_jstring(env, etag); + } + std::string modifiedStr; + if (modified != nullptr) { + modifiedStr = mbgl::android::std_string_from_jstring(env, modified); + } + std::string cacheControlStr; + if (cacheControl != nullptr) { + cacheControlStr = mbgl::android::std_string_from_jstring(env, cacheControl); + } + std::string expiresStr; + if (expires != nullptr) { + expiresStr = mbgl::android::std_string_from_jstring(env, expires); + } + jbyte* bodyData = env->GetByteArrayElements(body, nullptr); + std::string bodyStr(reinterpret_cast<char*>(bodyData), env->GetArrayLength(body)); + env->ReleaseByteArrayElements(body, bodyData, JNI_ABORT); + return request->onResponse(code, messageStr, etagStr, modifiedStr, cacheControlStr, expiresStr, bodyStr); +} + +} diff --git a/platform/default/http_request_curl.cpp b/platform/default/http_request_curl.cpp index 0f7f8c0ac5..e416034b40 100644 --- a/platform/default/http_request_curl.cpp +++ b/platform/default/http_request_curl.cpp @@ -11,12 +11,6 @@ #include <curl/curl.h> -#ifdef __ANDROID__ -#include <mbgl/android/jni.hpp> -#include <zip.h> -#include <openssl/ssl.h> -#endif - #include <queue> #include <map> #include <cassert> @@ -331,104 +325,6 @@ int HTTPCURLContext::startTimeout(CURLM * /* multi */, long timeout_ms, void *us // ------------------------------------------------------------------------------------------------- -#ifdef __ANDROID__ - -// This function is called to load the CA bundle -// from http://curl.haxx.se/libcurl/c/cacertinmem.html¯ -static CURLcode sslctx_function(CURL * /* curl */, void *sslctx, void * /* parm */) { - - int error = 0; - struct zip *apk = zip_open(mbgl::android::apkPath.c_str(), 0, &error); - if (apk == nullptr) { - return CURLE_SSL_CACERT_BADFILE; - } - - struct zip_file *apkFile = zip_fopen(apk, "assets/ca-bundle.crt", ZIP_FL_NOCASE); - if (apkFile == nullptr) { - zip_close(apk); - apk = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - struct zip_stat stat; - if (zip_stat(apk, "assets/ca-bundle.crt", ZIP_FL_NOCASE, &stat) != 0) { - zip_fclose(apkFile); - apkFile = nullptr; - zip_close(apk); - apk = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - if (stat.size > std::numeric_limits<int>::max()) { - zip_fclose(apkFile); - apkFile = nullptr; - zip_close(apk); - apk = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - const auto pem = std::make_unique<char[]>(stat.size); - - if (static_cast<zip_uint64_t>(zip_fread(apkFile, reinterpret_cast<void *>(pem.get()), stat.size)) != stat.size) { - zip_fclose(apkFile); - apkFile = nullptr; - zip_close(apk); - apk = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - // get a pointer to the X509 certificate store (which may be empty!) - X509_STORE *store = SSL_CTX_get_cert_store((SSL_CTX *)sslctx); - if (store == nullptr) { - return CURLE_SSL_CACERT_BADFILE; - } - - // get a BIO - BIO *bio = BIO_new_mem_buf(pem.get(), static_cast<int>(stat.size)); - if (bio == nullptr) { - store = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - // use it to read the PEM formatted certificate from memory into an X509 - // structure that SSL can use - X509 *cert = nullptr; - while (PEM_read_bio_X509(bio, &cert, 0, nullptr) != nullptr) { - if (cert == nullptr) { - BIO_free(bio); - bio = nullptr; - store = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - // add our certificate to this store - if (X509_STORE_add_cert(store, cert) == 0) { - X509_free(cert); - cert = nullptr; - BIO_free(bio); - bio = nullptr; - store = nullptr; - return CURLE_SSL_CACERT_BADFILE; - } - - X509_free(cert); - cert = nullptr; - } - - // decrease reference counts - BIO_free(bio); - bio = nullptr; - - zip_fclose(apkFile); - apkFile = nullptr; - zip_close(apk); - apk = nullptr; - - // all set to go - return CURLE_OK; -} -#endif - HTTPCURLRequest::HTTPCURLRequest(HTTPCURLContext* context_, const Resource& resource_, Callback callback_, uv_loop_t*, std::shared_ptr<const Response> response_) : HTTPRequestBase(resource_, callback_), context(context_), @@ -458,12 +354,7 @@ HTTPCURLRequest::HTTPCURLRequest(HTTPCURLContext* context_, const Resource& reso handleError(curl_easy_setopt(handle, CURLOPT_PRIVATE, this)); handleError(curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, error)); -#ifdef __ANDROID__ - handleError(curl_easy_setopt(handle, CURLOPT_SSLCERTTYPE, "PEM")); - handleError(curl_easy_setopt(handle, CURLOPT_SSL_CTX_FUNCTION, sslctx_function)); -#else handleError(curl_easy_setopt(handle, CURLOPT_CAINFO, "ca-bundle.crt")); -#endif handleError(curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1)); handleError(curl_easy_setopt(handle, CURLOPT_URL, resource.url.c_str())); handleError(curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, writeCallback)); diff --git a/scripts/android/configure.sh b/scripts/android/configure.sh index 667ea68911..70c8973a8b 100644 --- a/scripts/android/configure.sh +++ b/scripts/android/configure.sh @@ -1,8 +1,6 @@ #!/usr/bin/env bash BOOST_VERSION=1.57.0 -LIBCURL_VERSION=7.40.0 -OPENSSL_VERSION=1.0.1l LIBPNG_VERSION=1.6.16 JPEG_VERSION=v9a SQLITE_VERSION=3.8.8.1 @@ -11,4 +9,4 @@ ZLIB_VERSION=system NUNICODE_VERSION=1.5.1 LIBZIP_VERSION=0.11.2 -export MASON_ANDROID_ABI=${MASON_PLATFORM_VERSION}
\ No newline at end of file +export MASON_ANDROID_ABI=${MASON_PLATFORM_VERSION} diff --git a/scripts/android/defaults.mk b/scripts/android/defaults.mk index 436f9db710..29f047c887 100644 --- a/scripts/android/defaults.mk +++ b/scripts/android/defaults.mk @@ -1,7 +1,7 @@ HEADLESS ?= none PLATFORM ?= android ASSET ?= zip -HTTP ?= curl +HTTP ?= android CACHE ?= sqlite GYP_FLAVOR_SUFFIX=-android |