diff --git a/Makefile b/Makefile index b8369143d..f426de0c6 100644 --- a/Makefile +++ b/Makefile @@ -251,6 +251,10 @@ run-android-test-app-center: run-android-upload-to-bintray: gradle/configuration.gradle $(MBGL_ANDROID_GRADLE) -Pmapbox.abis=all :MapboxGLAndroidSDK:bintrayUpload +.PHONY: run-android-upload-to-nexus +run-android-upload-to-nexus: gradle/configuration.gradle + $(MBGL_ANDROID_GRADLE) -Pmapbox.abis=all :MapboxGLAndroidSDK:publishMapboxMapsSdkPublicationPublicationToNexusRepository + # Uploads the compiled Android SDK SNAPSHOT to oss.jfrog.org .PHONY: run-android-upload-to-artifactory run-android-upload-to-artifactory: gradle/configuration.gradle diff --git a/MapboxGLAndroidSDK/gradle.properties b/MapboxGLAndroidSDK/gradle.properties index cb3bea48c..c7dc679ae 100644 --- a/MapboxGLAndroidSDK/gradle.properties +++ b/MapboxGLAndroidSDK/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=9.1.0-SNAPSHOT +VERSION_NAME=9.2.1-RC4 # Only build native dependencies for the current ABI # See https://code.google.com/p/android/issues/detail?id=221098#c20 diff --git a/MapboxGLAndroidSDK/src/cpp/map_renderer.cpp b/MapboxGLAndroidSDK/src/cpp/map_renderer.cpp index d99c3730b..07a66f226 100644 --- a/MapboxGLAndroidSDK/src/cpp/map_renderer.cpp +++ b/MapboxGLAndroidSDK/src/cpp/map_renderer.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -37,17 +38,21 @@ std::shared_ptr MapRenderer::MailboxData::getMailbox() const noexcept { MapRenderer::~MapRenderer() = default; void MapRenderer::reset() { - destroyed = true; - - if (renderer) { - // Make sure to destroy the renderer on the GL Thread - auto self = ActorRef(*this, mailboxData.getMailbox()); - self.ask(&MapRenderer::resetRenderer).wait(); + try { + destroyed = true; + + if (renderer) { + // Make sure to destroy the renderer on the GL Thread + auto self = ActorRef(*this, mailboxData.getMailbox()); + self.ask(&MapRenderer::resetRenderer).wait(); + } + + // Lock to make sure there is no concurrent initialisation on the gl thread + std::lock_guard lock(initialisationMutex); + rendererObserver.reset(); + } catch (const std::exception& exception) { + Log::Error(Event::Android, "MapRenderer::reset failed: %s", exception.what()); } - - // Lock to make sure there is no concurrent initialisation on the gl thread - std::lock_guard lock(initialisationMutex); - rendererObserver.reset(); } ActorRef MapRenderer::actor() const { @@ -55,51 +60,67 @@ ActorRef MapRenderer::actor() const { } void MapRenderer::schedule(std::function scheduled) { - // Create a runnable - android::UniqueEnv _env = android::AttachEnv(); - auto runnable = std::make_unique(*_env, std::move(scheduled)); - - // Obtain ownership of the peer (gets transferred to the MapRenderer on the JVM for later GC) - auto peer = runnable->peer(); - - // Queue the event on the Java Peer - static auto& javaClass = jni::Class::Singleton(*_env); - static auto queueEvent = javaClass.GetMethod)>(*_env, "queueEvent"); - auto weakReference = javaPeer.get(*_env); - if (weakReference) { - weakReference.Call(*_env, queueEvent, peer); + try { + // Create a runnable + android::UniqueEnv _env = android::AttachEnv(); + auto runnable = std::make_unique(*_env, std::move(scheduled)); + + // Obtain ownership of the peer (gets transferred to the MapRenderer on the JVM for later GC) + auto peer = runnable->peer(); + + // Queue the event on the Java Peer + static auto& javaClass = jni::Class::Singleton(*_env); + static auto queueEvent = javaClass.GetMethod)>(*_env, "queueEvent"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, queueEvent, peer); + } + + // Release the c++ peer as it will be destroyed on GC of the Java Peer + runnable.release(); + } catch (const std::exception& exception) { + Log::Error(Event::Android, "MapRenderer::schedule failed: %s", exception.what()); } - - // Release the c++ peer as it will be destroyed on GC of the Java Peer - runnable.release(); } void MapRenderer::requestRender() { - android::UniqueEnv _env = android::AttachEnv(); - static auto& javaClass = jni::Class::Singleton(*_env); - static auto onInvalidate = javaClass.GetMethod(*_env, "requestRender"); - auto weakReference = javaPeer.get(*_env); - if (weakReference) { - weakReference.Call(*_env, onInvalidate); + try { + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onInvalidate = javaClass.GetMethod(*_env, "requestRender"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onInvalidate); + } + } catch (const std::exception& exception) { + Log::Error(Event::Android, "MapRenderer::requestRender failed: %s", exception.what()); } } void MapRenderer::update(std::shared_ptr params) { - // Lock on the parameters - std::lock_guard lock(updateMutex); - updateParameters = std::move(params); + try { + // Lock on the parameters + std::lock_guard lock(updateMutex); + updateParameters = std::move(params); + } catch (const std::exception& exception) { + Log::Error(Event::Android, "MapRenderer::update failed: %s", exception.what()); + } } void MapRenderer::setObserver(std::shared_ptr _rendererObserver) { - // Lock as the initialization can come from the main thread or the GL thread first - std::lock_guard lock(initialisationMutex); - - rendererObserver = std::move(_rendererObserver); - - // Set the new observer on the Renderer implementation - if (renderer) { - renderer->setObserver(rendererObserver.get()); + try { + // Lock as the initialization can come from the main thread or the GL thread first + std::lock_guard lock(initialisationMutex); + + rendererObserver = std::move(_rendererObserver); + + // Set the new observer on the Renderer implementation + if (renderer) { + renderer->setObserver(rendererObserver.get()); + } + } catch (const std::exception& exception) { + Log::Error(Event::Android, "MapRenderer::setObserver failed: %s", exception.what()); } } diff --git a/MapboxGLAndroidSDK/src/cpp/style/sources/custom_geometry_source.cpp b/MapboxGLAndroidSDK/src/cpp/style/sources/custom_geometry_source.cpp index e43bf8fa1..b0deacfa5 100644 --- a/MapboxGLAndroidSDK/src/cpp/style/sources/custom_geometry_source.cpp +++ b/MapboxGLAndroidSDK/src/cpp/style/sources/custom_geometry_source.cpp @@ -110,10 +110,10 @@ namespace android { static auto& javaClass = jni::Class::Singleton(*_env); static auto releaseThreads = javaClass.GetMethod(*_env, "releaseThreads"); - assert(javaPeer); - - auto peer = jni::Cast(*_env, javaClass, javaPeer); - peer.Call(*_env, releaseThreads); + if(javaPeer) { + auto peer = jni::Cast(*_env, javaClass, javaPeer); + peer.Call(*_env, releaseThreads); + } }; bool CustomGeometrySource::isCancelled(jni::jint z, diff --git a/MapboxGLAndroidSDK/src/cpp/style/sources/source.cpp b/MapboxGLAndroidSDK/src/cpp/style/sources/source.cpp index 39f352a84..2b0b4fb51 100644 --- a/MapboxGLAndroidSDK/src/cpp/style/sources/source.cpp +++ b/MapboxGLAndroidSDK/src/cpp/style/sources/source.cpp @@ -76,6 +76,10 @@ static std::unique_ptr createSourcePeer(jni::JNIEnv& env, } Source::~Source() { + if (ownedSource) { + ownedSource.reset(); + ownedSource.release(); + } // Before being added to a map, the Java peer owns this C++ peer and cleans // up after itself correctly through the jni native peer bindings. // After being added to the map, the ownership is flipped and the C++ peer has a strong reference @@ -209,7 +213,7 @@ static std::unique_ptr createSourcePeer(jni::JNIEnv& env, // Release the strong reference to the java peer assert(javaPeer); - javaPeer.release(); + javaPeer.reset(); rendererFrontend = nullptr; } diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java index ab8eec709..874074e70 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java @@ -114,9 +114,9 @@ void feedNewLocation(@NonNull @Size(min = 1) Location[] newLocations, // replace the animation start with the camera's previous value latLngValues[0] = previousCameraLatLng; if (isGpsNorth) { - bearingValues = new Float[] {previousCameraBearing, 0f}; + bearingValues = new Float[] {previousCameraBearing, shortestRotation(0f, previousCameraBearing)}; } else { - bearingValues[0] = previousCameraBearing; + bearingValues = getBearingValues(previousCameraBearing, newLocations); } updateCameraAnimators(latLngValues, bearingValues); diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java index a57f56164..c14590c20 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java @@ -530,21 +530,33 @@ public boolean onTouchEvent(MotionEvent event) { @Override public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + if (!isKeyDetectorInitialized()) { + return super.onKeyDown(keyCode, event); + } return mapKeyListener.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); } @Override public boolean onKeyLongPress(int keyCode, KeyEvent event) { + if (!isKeyDetectorInitialized()) { + return super.onKeyLongPress(keyCode, event); + } return mapKeyListener.onKeyLongPress(keyCode, event) || super.onKeyLongPress(keyCode, event); } @Override public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { + if (!isKeyDetectorInitialized()) { + return super.onKeyUp(keyCode, event); + } return mapKeyListener.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); } @Override public boolean onTrackballEvent(@NonNull MotionEvent event) { + if (!isKeyDetectorInitialized()) { + return super.onTrackballEvent(event); + } return mapKeyListener.onTrackballEvent(event) || super.onTrackballEvent(event); } @@ -1126,6 +1138,10 @@ private boolean isGestureDetectorInitialized() { return mapGestureDetector != null; } + private boolean isKeyDetectorInitialized() { + return mapKeyListener != null; + } + @Nullable MapboxMap getMapboxMap() { return mapboxMap; diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java index f50b1a394..e876f6ad6 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java @@ -81,6 +81,7 @@ public final class MapboxMap { private Style style; private boolean debugActive; + private boolean started; MapboxMap(NativeMap map, Transform transform, UiSettings ui, Projection projection, OnGesturesManagerInteractionListener listener, CameraChangeDispatcher cameraChangeDispatcher, @@ -143,6 +144,7 @@ public Style getStyle() { * Called when the hosting Activity/Fragment onStart() method is called. */ void onStart() { + started = true; locationComponent.onStart(); } @@ -150,6 +152,7 @@ void onStart() { * Called when the hosting Activity/Fragment onStop() method is called. */ void onStop() { + started = false; locationComponent.onStop(); } @@ -2003,6 +2006,9 @@ public OnInfoWindowCloseListener getOnInfoWindowCloseListener() { * @param callback Callback method invoked when the snapshot is taken. */ public void snapshot(@NonNull SnapshotReadyCallback callback) { + if (!started) { + return; + } nativeMapView.addSnapshotCallback(callback); } diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLConfigChooser.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLConfigChooser.java index 0a84464cf..a64becd2b 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLConfigChooser.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLConfigChooser.java @@ -2,13 +2,16 @@ import android.opengl.GLSurfaceView; import android.os.Build; + import androidx.annotation.NonNull; + import com.mapbox.mapboxsdk.constants.MapboxConstants; import com.mapbox.mapboxsdk.log.Logger; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLDisplay; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -74,7 +77,6 @@ public EGLConfig chooseConfig(@NonNull EGL10 egl, EGLDisplay display) { int[] numConfigs = getNumberOfConfigurations(egl, display, configAttribs); if (numConfigs[0] < 1) { Logger.e(TAG, "eglChooseConfig() returned no configs."); - throw new EGLConfigException("eglChooseConfig() failed"); } // Get all possible configurations @@ -84,7 +86,6 @@ public EGLConfig chooseConfig(@NonNull EGL10 egl, EGLDisplay display) { EGLConfig config = chooseBestMatchConfig(egl, display, possibleConfigurations); if (config == null) { Logger.e(TAG, "No config chosen"); - throw new EGLConfigException("No config chosen"); } return config; @@ -97,7 +98,6 @@ private int[] getNumberOfConfigurations(EGL10 egl, EGLDisplay display, int[] con Logger.e(TAG, String.format( MapboxConstants.MAPBOX_LOCALE, "eglChooseConfig(NULL) returned error %d", egl.eglGetError()) ); - throw new EGLConfigException("eglChooseConfig() failed"); } return numConfigs; } @@ -110,7 +110,6 @@ private EGLConfig[] getPossibleConfigurations(EGL10 egl, EGLDisplay display, Logger.e(TAG, String.format( MapboxConstants.MAPBOX_LOCALE, "eglChooseConfig() returned error %d", egl.eglGetError()) ); - throw new EGLConfigException("eglChooseConfig() failed"); } return configs; } @@ -255,7 +254,8 @@ public int compareTo(@NonNull Config other) { Collections.sort(matches); if (matches.size() == 0) { - throw new EGLConfigException("No matching configurations after filtering"); + Logger.e(TAG, "No matching configurations after filtering"); + return null; } Config bestMatch = matches.get(0); @@ -277,7 +277,6 @@ private int getConfigAttr(EGL10 egl, EGLDisplay display, EGLConfig config, int a Logger.e(TAG, String.format( MapboxConstants.MAPBOX_LOCALE, "eglGetConfigAttrib(%d) returned error %d", attributeName, egl.eglGetError()) ); - throw new EGLConfigException("eglGetConfigAttrib() failed"); } return attributevalue[0]; } diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLConfigException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLConfigException.java deleted file mode 100644 index d5a1c9951..000000000 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLConfigException.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.mapbox.mapboxsdk.maps.renderer.egl; - -/** - * Used for EGL configuration exceptions - */ -public class EGLConfigException extends RuntimeException { - public EGLConfigException() { - } - - public EGLConfigException(String message) { - super(message); - } - - public EGLConfigException(String message, Throwable cause) { - super(message, cause); - } - - public EGLConfigException(Throwable cause) { - super(cause); - } -} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLContextFactory.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLContextFactory.java new file mode 100644 index 000000000..2908c5f2e --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLContextFactory.java @@ -0,0 +1,30 @@ +package com.mapbox.mapboxsdk.maps.renderer.egl; + +import android.opengl.GLSurfaceView; +import android.util.Log; + +import androidx.annotation.Nullable; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; + +public class EGLContextFactory implements GLSurfaceView.EGLContextFactory { + + public EGLContext createContext(EGL10 egl, @Nullable EGLDisplay display, @Nullable EGLConfig config) { + if (display == null || config == null) { + return EGL10.EGL_NO_CONTEXT; + } + int[] attrib_list = {0x3098, 2, EGL10.EGL_NONE}; + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, attrib_list); + } + + public void destroyContext(EGL10 egl, EGLDisplay display, + EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLLogWrapper.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLLogWrapper.java new file mode 100644 index 000000000..0d84027a4 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLLogWrapper.java @@ -0,0 +1,546 @@ +package com.mapbox.mapboxsdk.maps.renderer.egl; + +import android.opengl.GLDebugHelper; +import android.opengl.GLException; + +import java.io.IOException; +import java.io.Writer; + +import javax.microedition.khronos.egl.EGL; +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +public class EGLLogWrapper implements EGL11 { + private EGL10 egl10; + private Writer log; + private boolean logArgumentNames; + private boolean checkError; + private int argCount; + + public EGLLogWrapper(EGL egl, int configFlags, Writer log) { + egl10 = (EGL10) egl; + this.log = log; + logArgumentNames = (GLDebugHelper.CONFIG_LOG_ARGUMENT_NAMES & configFlags) != 0; + checkError = (GLDebugHelper.CONFIG_CHECK_GL_ERROR & configFlags) != 0; + } + + public boolean eglChooseConfig(EGLDisplay display, int[] attrib_list, + EGLConfig[] configs, int config_size, int[] num_config) { + begin("eglChooseConfig"); + arg("display", display); + arg("attrib_list", attrib_list); + arg("config_size", config_size); + end(); + + boolean result = egl10.eglChooseConfig(display, attrib_list, configs, + config_size, num_config); + arg("configs", configs); + arg("num_config", num_config); + returns(result); + checkError(); + return result; + } + + public boolean eglCopyBuffers(EGLDisplay display, EGLSurface surface, + Object native_pixmap) { + begin("eglCopyBuffers"); + arg("display", display); + arg("surface", surface); + arg("native_pixmap", native_pixmap); + end(); + + boolean result = egl10.eglCopyBuffers(display, surface, native_pixmap); + returns(result); + checkError(); + return result; + } + + public EGLContext eglCreateContext(EGLDisplay display, EGLConfig config, + EGLContext share_context, int[] attrib_list) { + begin("eglCreateContext"); + arg("display", display); + arg("config", config); + arg("share_context", share_context); + arg("attrib_list", attrib_list); + end(); + + EGLContext result = egl10.eglCreateContext(display, config, + share_context, attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreatePbufferSurface(EGLDisplay display, + EGLConfig config, int[] attrib_list) { + begin("eglCreatePbufferSurface"); + arg("display", display); + arg("config", config); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = egl10.eglCreatePbufferSurface(display, config, + attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreatePixmapSurface(EGLDisplay display, + EGLConfig config, Object native_pixmap, int[] attrib_list) { + begin("eglCreatePixmapSurface"); + arg("display", display); + arg("config", config); + arg("native_pixmap", native_pixmap); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = egl10.eglCreatePixmapSurface(display, config, + native_pixmap, attrib_list); + returns(result); + checkError(); + return result; + } + + public EGLSurface eglCreateWindowSurface(EGLDisplay display, + EGLConfig config, Object native_window, int[] attrib_list) { + begin("eglCreateWindowSurface"); + arg("display", display); + arg("config", config); + arg("native_window", native_window); + arg("attrib_list", attrib_list); + end(); + + EGLSurface result = egl10.eglCreateWindowSurface(display, config, + native_window, attrib_list); + returns(result); + checkError(); + return result; + } + + public boolean eglDestroyContext(EGLDisplay display, EGLContext context) { + begin("eglDestroyContext"); + arg("display", display); + arg("context", context); + end(); + + boolean result = egl10.eglDestroyContext(display, context); + returns(result); + checkError(); + return result; + } + + public boolean eglDestroySurface(EGLDisplay display, EGLSurface surface) { + begin("eglDestroySurface"); + arg("display", display); + arg("surface", surface); + end(); + + boolean result = egl10.eglDestroySurface(display, surface); + returns(result); + checkError(); + return result; + } + + public boolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config, + int attribute, int[] value) { + begin("eglGetConfigAttrib"); + arg("display", display); + arg("config", config); + arg("attribute", attribute); + end(); + boolean result = egl10.eglGetConfigAttrib(display, config, attribute, + value); + arg("value", value); + returns(result); + checkError(); + return false; + } + + public boolean eglGetConfigs(EGLDisplay display, EGLConfig[] configs, + int config_size, int[] num_config) { + begin("eglGetConfigs"); + arg("display", display); + arg("config_size", config_size); + end(); + + boolean result = egl10.eglGetConfigs(display, configs, config_size, + num_config); + arg("configs", configs); + arg("num_config", num_config); + returns(result); + checkError(); + return result; + } + + public EGLContext eglGetCurrentContext() { + begin("eglGetCurrentContext"); + end(); + + EGLContext result = egl10.eglGetCurrentContext(); + returns(result); + + checkError(); + return result; + } + + public EGLDisplay eglGetCurrentDisplay() { + begin("eglGetCurrentDisplay"); + end(); + + EGLDisplay result = egl10.eglGetCurrentDisplay(); + returns(result); + + checkError(); + return result; + } + + public EGLSurface eglGetCurrentSurface(int readdraw) { + begin("eglGetCurrentSurface"); + arg("readdraw", readdraw); + end(); + + EGLSurface result = egl10.eglGetCurrentSurface(readdraw); + returns(result); + + checkError(); + return result; + } + + public EGLDisplay eglGetDisplay(Object native_display) { + begin("eglGetDisplay"); + arg("native_display", native_display); + end(); + + EGLDisplay result = egl10.eglGetDisplay(native_display); + returns(result); + + checkError(); + return result; + } + + public int eglGetError() { + begin("eglGetError"); + end(); + + int result = egl10.eglGetError(); + returns(getErrorString(result)); + + return result; + } + + public boolean eglInitialize(EGLDisplay display, int[] major_minor) { + begin("eglInitialize"); + arg("display", display); + end(); + boolean result = egl10.eglInitialize(display, major_minor); + returns(result); + arg("major_minor", major_minor); + checkError(); + return result; + } + + public boolean eglMakeCurrent(EGLDisplay display, EGLSurface draw, + EGLSurface read, EGLContext context) { + begin("eglMakeCurrent"); + arg("display", display); + arg("draw", draw); + arg("read", read); + arg("context", context); + end(); + boolean result = egl10.eglMakeCurrent(display, draw, read, context); + returns(result); + checkError(); + return result; + } + + public boolean eglQueryContext(EGLDisplay display, EGLContext context, + int attribute, int[] value) { + begin("eglQueryContext"); + arg("display", display); + arg("context", context); + arg("attribute", attribute); + end(); + boolean result = egl10.eglQueryContext(display, context, attribute, + value); + returns(value[0]); + returns(result); + checkError(); + return result; + } + + public String eglQueryString(EGLDisplay display, int name) { + begin("eglQueryString"); + arg("display", display); + arg("name", name); + end(); + String result = egl10.eglQueryString(display, name); + returns(result); + checkError(); + return result; + } + + public boolean eglQuerySurface(EGLDisplay display, EGLSurface surface, + int attribute, int[] value) { + begin("eglQuerySurface"); + arg("display", display); + arg("surface", surface); + arg("attribute", attribute); + end(); + boolean result = egl10.eglQuerySurface(display, surface, attribute, + value); + returns(value[0]); + returns(result); + checkError(); + return result; + } + + public boolean eglSwapBuffers(EGLDisplay display, EGLSurface surface) { + begin("eglSwapBuffers"); + arg("display", display); + arg("surface", surface); + end(); + boolean result = egl10.eglSwapBuffers(display, surface); + returns(result); + checkError(); + return result; + } + + public boolean eglTerminate(EGLDisplay display) { + begin("eglTerminate"); + arg("display", display); + end(); + boolean result = egl10.eglTerminate(display); + returns(result); + checkError(); + return result; + } + + public boolean eglWaitGL() { + begin("eglWaitGL"); + end(); + boolean result = egl10.eglWaitGL(); + returns(result); + checkError(); + return result; + } + + public boolean eglWaitNative(int engine, Object bindTarget) { + begin("eglWaitNative"); + arg("engine", engine); + arg("bindTarget", bindTarget); + end(); + boolean result = egl10.eglWaitNative(engine, bindTarget); + returns(result); + checkError(); + return result; + } + + private void checkError() { + int eglError; + if ((eglError = egl10.eglGetError()) != EGL_SUCCESS) { + String errorMessage = "eglError: " + getErrorString(eglError); + logLine(errorMessage); + if (checkError) { + throw new GLException(eglError, errorMessage); + } + } + } + + private void logLine(String message) { + log(message + '\n'); + } + + private void log(String message) { + try { + log.write(message); + } catch (IOException exception) { + // Ignore exception, keep on trying + } + } + + private void begin(String name) { + log(name + '('); + argCount = 0; + } + + private void arg(String name, String value) { + if (argCount++ > 0) { + log(", "); + } + if (logArgumentNames) { + log(name + "="); + } + log(value); + } + + private void end() { + log(");\n"); + flush(); + } + + private void flush() { + try { + log.flush(); + } catch (IOException exception) { + log = null; + } + } + + private void arg(String name, int value) { + arg(name, Integer.toString(value)); + } + + private void arg(String name, Object object) { + arg(name, toString(object)); + } + + private void arg(String name, EGLDisplay object) { + if (object == EGL10.EGL_DEFAULT_DISPLAY) { + arg(name, "EGL10.EGL_DEFAULT_DISPLAY"); + } else if (object == EGL_NO_DISPLAY) { + arg(name, "EGL10.EGL_NO_DISPLAY"); + } else { + arg(name, toString(object)); + } + } + + private void arg(String name, EGLContext object) { + if (object == EGL10.EGL_NO_CONTEXT) { + arg(name, "EGL10.EGL_NO_CONTEXT"); + } else { + arg(name, toString(object)); + } + } + + private void arg(String name, EGLSurface object) { + if (object == EGL10.EGL_NO_SURFACE) { + arg(name, "EGL10.EGL_NO_SURFACE"); + } else { + arg(name, toString(object)); + } + } + + private void returns(String result) { + log(" returns " + result + ";\n"); + flush(); + } + + private void returns(int result) { + returns(Integer.toString(result)); + } + + private void returns(boolean result) { + returns(Boolean.toString(result)); + } + + private void returns(Object result) { + returns(toString(result)); + } + + private String toString(Object obj) { + if (obj == null) { + return "null"; + } else { + return obj.toString(); + } + } + + private void arg(String name, int[] arr) { + if (arr == null) { + arg(name, "null"); + } else { + arg(name, toString(arr.length, arr, 0)); + } + } + + private void arg(String name, Object[] arr) { + if (arr == null) { + arg(name, "null"); + } else { + arg(name, toString(arr.length, arr, 0)); + } + } + + private String toString(int n, int[] arr, int offset) { + StringBuilder buf = new StringBuilder(); + buf.append("{\n"); + int arrLen = arr.length; + for (int i = 0; i < n; i++) { + int index = offset + i; + buf.append(" [" + index + "] = "); + if (index < 0 || index >= arrLen) { + buf.append("out of bounds"); + } else { + buf.append(arr[index]); + } + buf.append('\n'); + } + buf.append("}"); + return buf.toString(); + } + + private String toString(int n, Object[] arr, int offset) { + StringBuilder buf = new StringBuilder(); + buf.append("{\n"); + int arrLen = arr.length; + for (int i = 0; i < n; i++) { + int index = offset + i; + buf.append(" [" + index + "] = "); + if (index < 0 || index >= arrLen) { + buf.append("out of bounds"); + } else { + buf.append(arr[index]); + } + buf.append('\n'); + } + buf.append("}"); + return buf.toString(); + } + + private static String getHex(int value) { + return "0x" + Integer.toHexString(value); + } + + public static String getErrorString(int error) { + switch (error) { + case EGL_SUCCESS: + return "EGL_SUCCESS"; + case EGL_NOT_INITIALIZED: + return "EGL_NOT_INITIALIZED"; + case EGL_BAD_ACCESS: + return "EGL_BAD_ACCESS"; + case EGL_BAD_ALLOC: + return "EGL_BAD_ALLOC"; + case EGL_BAD_ATTRIBUTE: + return "EGL_BAD_ATTRIBUTE"; + case EGL_BAD_CONFIG: + return "EGL_BAD_CONFIG"; + case EGL_BAD_CONTEXT: + return "EGL_BAD_CONTEXT"; + case EGL_BAD_CURRENT_SURFACE: + return "EGL_BAD_CURRENT_SURFACE"; + case EGL_BAD_DISPLAY: + return "EGL_BAD_DISPLAY"; + case EGL_BAD_MATCH: + return "EGL_BAD_MATCH"; + case EGL_BAD_NATIVE_PIXMAP: + return "EGL_BAD_NATIVE_PIXMAP"; + case EGL_BAD_NATIVE_WINDOW: + return "EGL_BAD_NATIVE_WINDOW"; + case EGL_BAD_PARAMETER: + return "EGL_BAD_PARAMETER"; + case EGL_BAD_SURFACE: + return "EGL_BAD_SURFACE"; + case EGL11.EGL_CONTEXT_LOST: + return "EGL_CONTEXT_LOST"; + default: + return getHex(error); + } + } +} + diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLWindowSurfaceFactory.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLWindowSurfaceFactory.java new file mode 100644 index 000000000..845d7ae2e --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLWindowSurfaceFactory.java @@ -0,0 +1,38 @@ +package com.mapbox.mapboxsdk.maps.renderer.egl; + +import android.opengl.GLSurfaceView; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +public class EGLWindowSurfaceFactory implements GLSurfaceView.EGLWindowSurfaceFactory { + public EGLSurface createWindowSurface(@NonNull EGL10 egl, @Nullable EGLDisplay display, @Nullable EGLConfig config, + @Nullable Object nativeWindow) { + EGLSurface result = null; + if (display != null && config != null && nativeWindow != null) { + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (Exception exception) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e("EGLWindowSurfaceFactory", "eglCreateWindowSurface", exception); + } + } + return result; + } + + public void destroySurface(EGL10 egl, EGLDisplay display, + EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/GLSurfaceViewMapRenderer.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/GLSurfaceViewMapRenderer.java index c1836ebb4..21a4cb501 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/GLSurfaceViewMapRenderer.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/GLSurfaceViewMapRenderer.java @@ -2,10 +2,13 @@ import android.content.Context; import android.opengl.GLSurfaceView; + import androidx.annotation.NonNull; import com.mapbox.mapboxsdk.maps.renderer.MapRenderer; import com.mapbox.mapboxsdk.maps.renderer.egl.EGLConfigChooser; +import com.mapbox.mapboxsdk.maps.renderer.egl.EGLContextFactory; +import com.mapbox.mapboxsdk.maps.renderer.egl.EGLWindowSurfaceFactory; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -28,7 +31,8 @@ public GLSurfaceViewMapRenderer(Context context, String localIdeographFontFamily) { super(context, localIdeographFontFamily); this.glSurfaceView = glSurfaceView; - glSurfaceView.setEGLContextClientVersion(2); + glSurfaceView.setEGLContextFactory(new EGLContextFactory()); + glSurfaceView.setEGLWindowSurfaceFactory(new EGLWindowSurfaceFactory()); glSurfaceView.setEGLConfigChooser(new EGLConfigChooser()); glSurfaceView.setRenderer(this); glSurfaceView.setRenderMode(RENDERMODE_WHEN_DIRTY); diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/MapboxGLSurfaceView.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/MapboxGLSurfaceView.java index e110601b7..27f90be3a 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/MapboxGLSurfaceView.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/MapboxGLSurfaceView.java @@ -2,31 +2,86 @@ import android.content.Context; import android.opengl.GLSurfaceView; -import androidx.annotation.NonNull; import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import androidx.annotation.NonNull; + +import com.mapbox.mapboxsdk.maps.renderer.egl.EGLLogWrapper; + +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; + +import static android.opengl.GLSurfaceView.RENDERMODE_CONTINUOUSLY; /** * {@link GLSurfaceView} extension that notifies a listener when the view is detached from window, * which is the point of destruction of the GL thread. */ -public class MapboxGLSurfaceView extends GLSurfaceView { +public class MapboxGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback2 { + private static final String TAG = "GLSurfaceView"; + private static final GLThreadManager glThreadManager = new GLThreadManager(); + + private final WeakReference viewWeakReference = new WeakReference<>(this); + private GLThread glThread; + private GLSurfaceView.Renderer renderer; + private GLSurfaceView.EGLConfigChooser eglConfigChooser; + private GLSurfaceView.EGLContextFactory eglContextFactory; + private GLSurfaceView.EGLWindowSurfaceFactory eglWindowSurfaceFactory; private OnGLSurfaceViewDetachedListener detachedListener; + private boolean preserveEGLContextOnPause; + private boolean detached; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ public MapboxGLSurfaceView(Context context) { super(context); + init(); } + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ public MapboxGLSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); + init(); + } + + private void init() { + // Install a SurfaceHolder.Callback so we get notified when the + // underlying surface is created and destroyed + SurfaceHolder holder = getHolder(); + holder.addCallback(this); } @Override - protected void onDetachedFromWindow() { - if (detachedListener != null) { - detachedListener.onGLSurfaceViewDetached(); + protected void finalize() throws Throwable { + try { + if (glThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + glThread.requestExitAndWait(); + } + } finally { + super.finalize(); } - super.onDetachedFromWindow(); } /** @@ -41,6 +96,1020 @@ public void setDetachedListener(@NonNull OnGLSurfaceViewDetachedListener detache this.detachedListener = detachedListener; } + /** + * Control whether the EGL context is preserved when the GLSurfaceView is paused and + * resumed. + *

+ * If set to true, then the EGL context may be preserved when the GLSurfaceView is paused. + *

+ * Prior to API level 11, whether the EGL context is actually preserved or not + * depends upon whether the Android device can support an arbitrary number of + * EGL contexts or not. Devices that can only support a limited number of EGL + * contexts must release the EGL context in order to allow multiple applications + * to share the GPU. + *

+ * If set to false, the EGL context will be released when the GLSurfaceView is paused, + * and recreated when the GLSurfaceView is resumed. + *

+ *

+ * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + preserveEGLContextOnPause = preserveOnPause; + } + + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return preserveEGLContextOnPause; + } + + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + *

This method should be called once and only once in the life-cycle of + * a GLSurfaceView. + *

The following GLSurfaceView methods can only be called before + * setRenderer is called: + *

    + *
  • {@link #setEGLConfigChooser(GLSurfaceView.EGLConfigChooser)} + *
+ *

+ * The following GLSurfaceView methods can only be called after + * setRenderer is called: + *

    + *
  • {@link #getRenderMode()} + *
  • {@link #onPause()} + *
  • {@link #onResume()} + *
  • {@link #queueEvent(Runnable)} + *
  • {@link #requestRender()} + *
  • {@link #setRenderMode(int)} + *
+ * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(GLSurfaceView.Renderer renderer) { + checkRenderThreadState(); + if (eglConfigChooser == null) { + throw new IllegalStateException("No eglConfigChooser provided"); + } + if (eglContextFactory == null) { + throw new IllegalStateException("No eglContextFactory provided"); + } + if (eglWindowSurfaceFactory == null) { + throw new IllegalStateException("No eglWindowSurfaceFactory provided"); + } + this.renderer = renderer; + glThread = new GLThread(viewWeakReference); + glThread.start(); + } + + /** + * Install a custom EGLContextFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(GLSurfaceView.Renderer)} + * is called. + */ + public void setEGLContextFactory(GLSurfaceView.EGLContextFactory factory) { + checkRenderThreadState(); + eglContextFactory = factory; + } + + /** + * Install a custom EGLWindowSurfaceFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(GLSurfaceView.Renderer)} + * is called. + */ + public void setEGLWindowSurfaceFactory(GLSurfaceView.EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + eglWindowSurfaceFactory = factory; + } + + /** + * Install a custom EGLConfigChooser. + *

If this method is + * called, it must be called before {@link #setRenderer(GLSurfaceView.Renderer)} + * is called. + */ + public void setEGLConfigChooser(GLSurfaceView.EGLConfigChooser configChooser) { + checkRenderThreadState(); + eglConfigChooser = configChooser; + } + + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + *

+ * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + *

+ * This method can only be called after {@link #setRenderer(GLSurfaceView.Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + */ + public void setRenderMode(int renderMode) { + glThread.setRenderMode(renderMode); + } + + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * + * @return the current rendering mode. + */ + public int getRenderMode() { + return glThread.getRenderMode(); + } + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * RENDERMODE_WHEN_DIRTY, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + public void requestRender() { + glThread.requestRender(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceCreated(SurfaceHolder holder) { + glThread.surfaceCreated(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceDestroyed(SurfaceHolder holder) { + // Surface will be destroyed when we return + glThread.surfaceDestroyed(); + } + + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + glThread.onWindowResize(w, h); + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceRedrawNeededAsync(SurfaceHolder holder, Runnable finishDrawing) { + if (glThread != null) { + glThread.requestRenderAndNotify(finishDrawing); + } + } + + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Deprecated + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + // Since we are part of the framework we know only surfaceRedrawNeededAsync + // will be called. + } + + /** + * Pause the rendering thread, optionally tearing down the EGL context + * depending upon the value of {@link #setPreserveEGLContextOnPause(boolean)}. + *

+ * Must not be called before a renderer has been set. + */ + public void onPause() { + glThread.onPause(); + } + + /** + * Resumes the rendering thread, re-creating the OpenGL context if necessary. It + * is the counterpart to {@link #onPause()}. + *

+ * Must not be called before a renderer has been set. + */ + public void onResume() { + glThread.onResume(); + } + + /** + * Queue a runnable to be run on the GL rendering thread. This can be used + * to communicate with the Renderer on the rendering thread. + * Must not be called before a renderer has been set. + * + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + glThread.queueEvent(r); + } + + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLSurfaceView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (detached && (renderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (glThread != null) { + renderMode = glThread.getRenderMode(); + } + glThread = new GLThread(viewWeakReference); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + glThread.setRenderMode(renderMode); + } + glThread.start(); + } + detached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (detachedListener != null) { + detachedListener.onGLSurfaceViewDetached(); + } + if (glThread != null) { + glThread.requestExitAndWait(); + } + detached = true; + super.onDetachedFromWindow(); + } + + /** + * An EGL helper class. + */ + private static class EglHelper { + private EglHelper(WeakReference glSurfaceViewWeakRef) { + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + /** + * Initialize EGL for a given configuration spec. + */ + public void start() { + try { + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + Log.e(TAG, "eglGetDisplay failed"); + return; + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if (!mEgl.eglInitialize(mEglDisplay, version)) { + Log.e(TAG, "eglInitialize failed"); + return; + } + MapboxGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = view.eglConfigChooser.chooseConfig(mEgl, mEglDisplay); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = view.eglContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + Log.e(TAG, "createContext failed"); + return; + } + } catch (Exception exception) { + Log.e(TAG, "createContext failed: ", exception); + } + mEglSurface = null; + } + + /** + * Create an egl surface for the current SurfaceHolder surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + boolean createSurface() { + /* + * Check preconditions. + */ + if (mEgl == null) { + Log.e(TAG, "egl not initialized"); + return false; + } + if (mEglDisplay == null) { + Log.e(TAG, "eglDisplay not initialized"); + return false; + } + if (mEglConfig == null) { + Log.e(TAG, "mEglConfig not initialized"); + return false; + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + MapboxGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + mEglSurface = view.eglWindowSurfaceFactory.createWindowSurface(mEgl, + mEglDisplay, mEglConfig, view.getHolder()); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e(TAG, "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning(TAG, "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + */ + GL createGL() { + return mEglContext.getGL(); + } + + /** + * Display the current render surface. + * + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + void destroySurface() { + destroySurfaceImp(); + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + MapboxGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.eglWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (mEglContext != null) { + MapboxGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.eglContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + static String formatEglError(String function, int error) { + return function + " failed: " + EGLLogWrapper.getErrorString(error); + } + + private WeakReference mGLSurfaceViewWeakRef; + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + + } + + /** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + *

+ * All potentially blocking synchronization is done through the + * sGLThreadManager object. This avoids multiple-lock ordering issues. + */ + static class GLThread extends Thread { + GLThread(WeakReference glSurfaceViewWeakRef) { + super(); + width = 0; + height = 0; + requestRender = true; + renderMode = RENDERMODE_CONTINUOUSLY; + wantRenderNotification = false; + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + + try { + guardedRun(); + } catch (InterruptedException exception) { + // fall thru and exit normally + } finally { + glThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (haveEglSurface) { + haveEglSurface = false; + eglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (haveEglContext) { + eglHelper.finish(); + haveEglContext = false; + glThreadManager.releaseEglContextLocked(this); + } + } + + private void guardedRun() throws InterruptedException { + eglHelper = new EglHelper(mGLSurfaceViewWeakRef); + haveEglContext = false; + haveEglSurface = false; + wantRenderNotification = false; + + try { + GL10 gl = null; + boolean createEglContext = false; + boolean createEglSurface = false; + boolean createGlInterface = false; + boolean lostEglContext = false; + boolean sizeChanged = false; + boolean wantRenderNotification = false; + boolean doRenderNotification = false; + boolean askedToReleaseEglContext = false; + int w = 0; + int h = 0; + Runnable event = null; + Runnable finishDrawingRunnable = null; + + while (true) { + synchronized (glThreadManager) { + while (true) { + if (shouldExit) { + return; + } + + if (!eventQueue.isEmpty()) { + event = eventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (paused != requestPaused) { + pausing = requestPaused; + paused = requestPaused; + glThreadManager.notifyAll(); + } + + // Do we need to give up the EGL context? + if (shouldReleaseEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + shouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && haveEglSurface) { + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && haveEglContext) { + MapboxGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + boolean preserveEglContextOnPause = view != null && view.preserveEGLContextOnPause; + if (!preserveEglContextOnPause) { + stopEglContextLocked(); + } + } + + // Have we lost the SurfaceView surface? + if ((!hasSurface) && (!waitingForSurface)) { + if (haveEglSurface) { + stopEglSurfaceLocked(); + } + waitingForSurface = true; + surfaceIsBad = false; + glThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (hasSurface && waitingForSurface) { + waitingForSurface = false; + glThreadManager.notifyAll(); + } + + if (doRenderNotification) { + this.wantRenderNotification = false; + doRenderNotification = false; + renderComplete = true; + glThreadManager.notifyAll(); + } + + if (this.finishDrawingRunnable != null) { + finishDrawingRunnable = this.finishDrawingRunnable; + this.finishDrawingRunnable = null; + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (!haveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else { + try { + eglHelper.start(); + } catch (RuntimeException exception) { + glThreadManager.releaseEglContextLocked(this); + return; + } + haveEglContext = true; + createEglContext = true; + + glThreadManager.notifyAll(); + } + } + + if (haveEglContext && !haveEglSurface) { + haveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (haveEglSurface) { + if (this.sizeChanged) { + sizeChanged = true; + w = width; + h = height; + this.wantRenderNotification = true; + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + this.sizeChanged = false; + } + requestRender = false; + glThreadManager.notifyAll(); + if (this.wantRenderNotification) { + wantRenderNotification = true; + } + break; + } + } else { + if (finishDrawingRunnable != null) { + Log.w(TAG, "Warning, !readyToDraw() but waiting for " + + "draw finished! Early reporting draw finished."); + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + // By design, this is the only place in a GLThread thread where we wait(). + glThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (eglHelper.createSurface()) { + synchronized (glThreadManager) { + finishedCreatingEglSurface = true; + glThreadManager.notifyAll(); + } + } else { + synchronized (glThreadManager) { + finishedCreatingEglSurface = true; + surfaceIsBad = true; + glThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + + if (createGlInterface) { + gl = (GL10) eglHelper.createGL(); + + createGlInterface = false; + } + + if (createEglContext) { + MapboxGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.renderer.onSurfaceCreated(gl, eglHelper.mEglConfig); + } + createEglContext = false; + } + + if (sizeChanged) { + MapboxGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.renderer.onSurfaceChanged(gl, w, h); + } + sizeChanged = false; + } + + MapboxGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.renderer.onDrawFrame(gl); + if (finishDrawingRunnable != null) { + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + int swapError = eglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the SurfaceView surface has been destroyed, + // but we haven't been notified yet. + // Log the error to help developers understand why rendering stopped. + EglHelper.logEglErrorAsWarning(TAG, "eglSwapBuffers", swapError); + + synchronized (glThreadManager) { + surfaceIsBad = true; + glThreadManager.notifyAll(); + } + break; + } + + if (wantRenderNotification) { + doRenderNotification = true; + wantRenderNotification = false; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (glThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return haveEglContext && haveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + return (!paused) && hasSurface && (!surfaceIsBad) + && (width > 0) && (height > 0) + && (requestRender || (renderMode == RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + synchronized (glThreadManager) { + this.renderMode = renderMode; + glThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized (glThreadManager) { + return renderMode; + } + } + + public void requestRender() { + synchronized (glThreadManager) { + requestRender = true; + glThreadManager.notifyAll(); + } + } + + public void requestRenderAndNotify(Runnable finishDrawing) { + synchronized (glThreadManager) { + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We will return to the client rendering code, so here we don't need to + // do anything. + if (Thread.currentThread() == this) { + return; + } + + wantRenderNotification = true; + requestRender = true; + renderComplete = false; + finishDrawingRunnable = finishDrawing; + + glThreadManager.notifyAll(); + } + } + + public void surfaceCreated() { + synchronized (glThreadManager) { + hasSurface = true; + finishedCreatingEglSurface = false; + glThreadManager.notifyAll(); + while (waitingForSurface + && !finishedCreatingEglSurface + && !exited) { + try { + glThreadManager.wait(); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed() { + synchronized (glThreadManager) { + hasSurface = false; + glThreadManager.notifyAll(); + while ((!waitingForSurface) && (!exited)) { + try { + glThreadManager.wait(); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause() { + synchronized (glThreadManager) { + requestPaused = true; + glThreadManager.notifyAll(); + while ((!exited) && (!paused)) { + try { + glThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (glThreadManager) { + requestPaused = false; + requestRender = true; + renderComplete = false; + glThreadManager.notifyAll(); + while ((!exited) && paused && (!renderComplete)) { + try { + glThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (glThreadManager) { + width = w; + height = h; + sizeChanged = true; + requestRender = true; + renderComplete = false; + + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We need to process the size change eventually though and update our EGLSurface. + // So we set the parameters and return so they can be processed on our + // next iteration. + if (Thread.currentThread() == this) { + return; + } + + glThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (!exited && !paused && !renderComplete + && ableToDraw()) { + try { + glThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized (glThreadManager) { + shouldExit = true; + glThreadManager.notifyAll(); + while (!exited) { + try { + glThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + shouldReleaseEglContext = true; + glThreadManager.notifyAll(); + } + + /** + * Queue an "event" to be run on the GL rendering thread. + * + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(@NonNull Runnable r) { + synchronized (glThreadManager) { + eventQueue.add(r); + glThreadManager.notifyAll(); + } + } + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean shouldExit; + private boolean exited; + private boolean requestPaused; + private boolean paused; + private boolean hasSurface; + private boolean surfaceIsBad; + private boolean waitingForSurface; + private boolean haveEglContext; + private boolean haveEglSurface; + private boolean finishedCreatingEglSurface; + private boolean shouldReleaseEglContext; + private int width; + private int height; + private int renderMode; + private boolean requestRender; + private boolean wantRenderNotification; + private boolean renderComplete; + private ArrayList eventQueue = new ArrayList<>(); + private boolean sizeChanged = true; + private Runnable finishDrawingRunnable = null; + // End of member variables protected by the sGLThreadManager monitor. + + private EglHelper eglHelper; + + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the GLSurfaceView to be garbage collected while + * the GLThread is still alive. + */ + private WeakReference mGLSurfaceViewWeakRef; + + } + + static class LogWriter extends Writer { + + @Override + public void close() { + flushBuilder(); + } + + @Override + public void flush() { + flushBuilder(); + } + + @Override + public void write(char[] buf, int offset, int count) { + for (int i = 0; i < count; i++) { + char c = buf[offset + i]; + if (c == '\n') { + flushBuilder(); + } else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v("GLSurfaceView", mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + + private StringBuilder mBuilder = new StringBuilder(); + } + + private void checkRenderThreadState() { + if (glThread != null) { + throw new IllegalStateException("setRenderer has already been called for this instance."); + } + } + + private static class GLThreadManager { + + synchronized void threadExiting(GLThread thread) { + thread.exited = true; + notifyAll(); + } + + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + void releaseEglContextLocked(GLThread thread) { + notifyAll(); + } + } + /** * Listener interface that notifies when a {@link MapboxGLSurfaceView} is detached from window. */ diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java index e2616360c..e3199ed5a 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java @@ -90,7 +90,8 @@ public void executeRequest(HttpResponder httpRequest, long nativePtr, @NonNull S public void cancelRequest() { // call can be null if the constructor gets aborted (e.g, under a NoRouteToHostException). if (call != null) { - HttpLogger.log(Log.DEBUG, String.format("[HTTP] Cancel request %s", call.request().url())); + HttpLogger.log(Log.DEBUG, String.format("[HTTP] This request was cancelled (%s). This is expected for tiles" + + " that were being prefetched but are no longer needed for the map to render.", call.request().url())); call.cancel(); } } diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt index bf479c973..d3f73f3d2 100644 --- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt @@ -167,6 +167,35 @@ class LocationAnimatorCoordinatorTest { } } + @Test + fun feedNewLocation_animatorValue_bearing() { + val previous = Location("") + previous.latitude = 51.1 + previous.longitude = 17.1 + previous.bearing = 355f + + val current = Location("") + current.latitude = 51.2 + current.longitude = 17.2 + current.bearing = 0f + + locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, false, false) + + verify { + animatorProvider.floatAnimator( + arrayOf(0f, -5f, 0f), any(), any() + ) + } + + locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, true, false) + + verify { + animatorProvider.floatAnimator( + arrayOf(0f, 0f), any(), any() + ) + } + } + @Test fun feedNewLocation_animatorValue_multiplePoints_animationDuration() { every { projection.getMetersPerPixelAtLatitude(any()) } answers { 10000.0 } // disable snap diff --git a/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RenderViewGetterTest.kt b/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RenderViewGetterTest.kt index a898fe024..48d435f9b 100644 --- a/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RenderViewGetterTest.kt +++ b/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RenderViewGetterTest.kt @@ -1,6 +1,5 @@ package com.mapbox.mapboxsdk.testapp.maps -import android.opengl.GLSurfaceView import android.view.TextureView import android.view.ViewGroup import androidx.test.annotation.UiThreadTest @@ -9,6 +8,7 @@ import androidx.test.rule.ActivityTestRule import com.mapbox.mapboxsdk.AppCenter import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.MapboxMapOptions +import com.mapbox.mapboxsdk.maps.renderer.glsurfaceview.MapboxGLSurfaceView import com.mapbox.mapboxsdk.testapp.activity.FeatureOverviewActivity import java.util.concurrent.CountDownLatch import junit.framework.Assert.assertNotNull @@ -34,7 +34,7 @@ class RenderViewGetterTest : AppCenter() { rootView = rule.activity.findViewById(android.R.id.content) mapView = MapView(rule.activity) assertNotNull(mapView.renderView) - assertTrue(mapView.renderView is GLSurfaceView) + assertTrue(mapView.renderView is MapboxGLSurfaceView) } @Test diff --git a/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/GLSurfaceRecyclerViewActivity.kt b/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/GLSurfaceRecyclerViewActivity.kt index 750af7cac..bb64b291a 100644 --- a/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/GLSurfaceRecyclerViewActivity.kt +++ b/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/GLSurfaceRecyclerViewActivity.kt @@ -6,7 +6,6 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.RecyclerView import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.Style import com.mapbox.mapboxsdk.testapp.R @@ -141,7 +140,11 @@ open class GLSurfaceRecyclerViewActivity : AppCompatActivity() { } fun bind(mapItem: MapItem) { - mapView.getMapAsync { mapboxMap -> mapboxMap.setStyle(mapItem.style) } + mapView.getMapAsync { mapboxMap -> + mapboxMap.setStyle(mapItem.style) { + mapboxMap.snapshot { } + } + } } } diff --git a/gradle/artifact-settings.gradle b/gradle/artifact-settings.gradle index acf29eeb8..574c7d547 100644 --- a/gradle/artifact-settings.gradle +++ b/gradle/artifact-settings.gradle @@ -18,4 +18,8 @@ ext { mapboxBintrayUser = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER') mapboxBintrayApiKey = project.hasProperty('BINTRAY_API_KEY') ? project.property('BINTRAY_API_KEY') : System.getenv('BINTRAY_API_KEY') mapboxGpgPassphrase = project.hasProperty('GPG_PASSPHRASE') ? project.property('GPG_PASSPHRASE') : System.getenv('GPG_PASSPHRASE') + + nexusUrl = project.hasProperty('NEXUS_URL') ? project.property('NEXUS_URL') : System.getenv('NEXUS_URL') + nexusUser = project.hasProperty('NEXUS_USER') ? project.property('NEXUS_USER') : System.getenv('NEXUS_USER') + nexusPassword = project.hasProperty('NEXUS_PASSWORD') ? project.property('NEXUS_PASSWORD') : System.getenv('NEXUS_PASSWORD') } diff --git a/gradle/gradle-bintray.gradle b/gradle/gradle-bintray.gradle index e8ace7359..a24775b45 100644 --- a/gradle/gradle-bintray.gradle +++ b/gradle/gradle-bintray.gradle @@ -42,6 +42,16 @@ publishing { } } } + repositories { + maven { + name 'nexus' + url project.ext.nexusUrl + credentials { + username project.ext.nexusUser + password project.ext.nexusPassword + } + } + } } bintray { diff --git a/vendor/mapbox-gl-native b/vendor/mapbox-gl-native index d60fd302b..b023aab0d 160000 --- a/vendor/mapbox-gl-native +++ b/vendor/mapbox-gl-native @@ -1 +1 @@ -Subproject commit d60fd302b1f6563e7d16952f8855122fdcc85f73 +Subproject commit b023aab0d00b7cd382c81bcbebcb419f8135e0e6