From 0885aed0c58fdec731065e189a8c31c98893131d Mon Sep 17 00:00:00 2001 From: Unity Ads Travis Date: Wed, 30 Jan 2019 17:46:05 +0000 Subject: [PATCH] Release 3.0.1 --- .editorconfig | 14 + Jenkinsfile | 6 + Makefile | 6 +- .../unity3d/ads/example/UnityAdsExample.java | 4 + .../ads/example/UnityMonetizationExample.java | 8 +- lib/build.gradle | 15 +- .../unity3d/services/ads/api/Placement.java | 6 + .../java/com/unity3d/services/ar/api/AR.java | 27 + .../com/unity3d/services/ar/view/ARView.java | 32 +- .../services/ar/view/GLSurfaceView.java | 1973 +++++++++++++++++ .../services/banners/UnityBanners.java | 5 + .../unity3d/services/banners/api/Banner.java | 16 +- .../services/banners/view/BannerPosition.java | 2 + .../com/unity3d/services/core/api/Intent.java | 8 +- .../unity3d/services/core/api/Storage.java | 6 +- .../services/core/cache/CacheDirectory.java | 18 + .../core/configuration/Configuration.java | 2 +- .../unity3d/services/core/device/Storage.java | 19 +- .../unity3d/services/core/misc/Utilities.java | 49 - .../core/properties/ClientProperties.java | 10 + .../services/core/request/WebRequest.java | 9 +- .../core/request/WebRequestRunnable.java | 2 +- .../purchasing/PromoMetadataUtilities.java | 2 +- scripts/build.sh | 4 + scripts/extra.groovy | 63 + scripts/test.sh | 4 + 26 files changed, 2215 insertions(+), 95 deletions(-) create mode 100644 .editorconfig create mode 100644 Jenkinsfile create mode 100644 lib/src/main/java/com/unity3d/services/ar/view/GLSurfaceView.java create mode 100755 scripts/build.sh create mode 100644 scripts/extra.groovy create mode 100755 scripts/test.sh diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3c32f6e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=false +indent_style=space +indent_size=4 + +[*.java] +indent_style=tab +tab_width=4 + +[{*.yml,*.yaml}] +indent_style=space +indent_size=2 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..51eeda6 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,6 @@ +@Library('applifier-shared-libs@master') _ + +Script { + extraSteps = ['scripts/extra.groovy'] + disable_registry = true +} diff --git a/Makefile b/Makefile index 418445d..edc5454 100644 --- a/Makefile +++ b/Makefile @@ -56,8 +56,8 @@ zip: release rm unity-ads.aar use-local-webview: - sed -i '' 's/return "https:\/\/config.unityads.unity3d.com\/webview\/" + getWebViewBranch() + "\/" + flavor + "\/config.json";/return "new-ip";/' "lib/src/main/java/com/unity3d/ads/properties/SdkProperties.java" - sed -i '' 's/return ".*";/return "http:\/\/$(shell ifconfig |grep "inet" |grep -E -o "([0-9]{1,3}[\.]){3}[0-9]{1,3}" |grep -v -E "^0|^127" -m 1):8000\/build\/dev\/config.json";/' "lib/src/main/java/com/unity3d/ads/properties/SdkProperties.java" + sed -i '' 's/return "https:\/\/config.unityads.unity3d.com\/webview\/" + getWebViewBranch() + "\/" + flavor + "\/config.json";/return "new-ip";/' "lib/src/main/java/com/unity3d/services/core/properties/SdkProperties.java" + sed -i '' 's/return ".*";/return "http:\/\/$(shell ifconfig |grep "inet" |grep -E -o "([0-9]{1,3}[\.]){3}[0-9]{1,3}" |grep -v -E "^0|^127" -m 1):8000\/build\/" + flavor + "\/config.json";/' "lib/src/main/java/com/unity3d/services/core/properties/SdkProperties.java" use-public-webview: - sed -i '' 's/return ".*";/return "https:\/\/config.unityads.unity3d.com\/webview\/" + getWebViewBranch() + "\/" + flavor + "\/config.json";/' "lib/src/main/java/com/unity3d/ads/properties/SdkProperties.java" + sed -i '' 's/return ".*";/return "https:\/\/config.unityads.unity3d.com\/webview\/" + getWebViewBranch() + "\/" + flavor + "\/config.json";/' "lib/src/main/java/com/unity3d/services/core/properties/SdkProperties.java" diff --git a/app/src/main/java/com/unity3d/ads/example/UnityAdsExample.java b/app/src/main/java/com/unity3d/ads/example/UnityAdsExample.java index 8d5d4c6..ca2fabe 100644 --- a/app/src/main/java/com/unity3d/ads/example/UnityAdsExample.java +++ b/app/src/main/java/com/unity3d/ads/example/UnityAdsExample.java @@ -29,6 +29,7 @@ public class UnityAdsExample extends Activity { private String interstitialPlacementId; private String incentivizedPlacementId; + private Button bannerButton; private static int ordinal = 1; @@ -93,6 +94,9 @@ public class UnityAdsExample extends Activity { } }); + bannerButton = (Button) findViewById(R.id.unityads_example_banner_button); + disableButton(bannerButton); + final Button initializeButton = (Button) findViewById(R.id.unityads_example_initialize_button); final EditText gameIdEdit = (EditText) findViewById(R.id.unityads_example_gameid_edit); final CheckBox testModeCheckbox = (CheckBox) findViewById(R.id.unityads_example_testmode_checkbox); diff --git a/app/src/main/java/com/unity3d/ads/example/UnityMonetizationExample.java b/app/src/main/java/com/unity3d/ads/example/UnityMonetizationExample.java index d856fa6..a48042c 100644 --- a/app/src/main/java/com/unity3d/ads/example/UnityMonetizationExample.java +++ b/app/src/main/java/com/unity3d/ads/example/UnityMonetizationExample.java @@ -24,6 +24,7 @@ import com.unity3d.ads.metadata.PlayerMetaData; import com.unity3d.services.UnityServices; import com.unity3d.services.banners.IUnityBannerListener; import com.unity3d.services.banners.UnityBanners; +import com.unity3d.services.banners.view.BannerPosition; import com.unity3d.services.core.log.DeviceLog; import com.unity3d.services.core.properties.SdkProperties; import com.unity3d.services.core.webview.WebView; @@ -155,11 +156,12 @@ public class UnityMonetizationExample extends Activity { }); bannerButton = (Button) findViewById(R.id.unityads_example_banner_button); - enableButton(bannerButton); + disableButton(bannerButton); bannerButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (bannerView == null) { + UnityBanners.setBannerPosition(BannerPosition.BOTTOM_CENTER); UnityBanners.loadBanner(thisActivity, bannerPlacementId); } else { UnityBanners.destroy(); @@ -196,6 +198,8 @@ public class UnityMonetizationExample extends Activity { SharedPreferences.Editor preferencesEdit = preferences.edit(); preferencesEdit.putString("gameId", gameId); preferencesEdit.commit(); + + enableButton(bannerButton); } }); @@ -230,6 +234,8 @@ public class UnityMonetizationExample extends Activity { else { disableButton((Button) findViewById(R.id.unityads_example_incentivized_button)); } + + enableButton(bannerButton); } } diff --git a/lib/build.gradle b/lib/build.gradle index b1ce8a7..9a7560d 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -11,8 +11,18 @@ android { defaultConfig { minSdkVersion 9 targetSdkVersion 26 - versionCode = 3000 - versionName = "3.0.0" + /* + Please ensure that the last two digits of version number does not exceed 50 unless + it is a China SDK. This is because adding 50 to the version number is a one-to-one + mapping between normal SDK and China SDK. + + Example : version number 2350 corresponds to China version of 2300. + + All SDK with version numbers with last two digits >= 50 will be treated + as China SDK for filtering in the backend. + */ + versionCode = 3001 + versionName = "3.0.1" setProperty("archivesBaseName", "unity-ads") @@ -77,6 +87,7 @@ android.libraryVariants.all { variant -> linksOffline "http://developer.android.com/reference","${android.sdkDirectory}/docs/reference" } exclude '**/R.java' + exclude 'com/unity3d/services/ar/view/GLSurfaceView.java' destinationDir = file("../javadoc/") } } diff --git a/lib/src/main/java/com/unity3d/services/ads/api/Placement.java b/lib/src/main/java/com/unity3d/services/ads/api/Placement.java index ddf9fd3..7e971b0 100644 --- a/lib/src/main/java/com/unity3d/services/ads/api/Placement.java +++ b/lib/src/main/java/com/unity3d/services/ads/api/Placement.java @@ -18,4 +18,10 @@ public class Placement { callback.invoke(); } + + @WebViewExposed + public static void setDefaultBannerPlacement(String placement, WebViewCallback callback) { + com.unity3d.services.ads.placement.Placement.setDefaultBannerPlacement(placement); + callback.invoke(); + } } \ No newline at end of file diff --git a/lib/src/main/java/com/unity3d/services/ar/api/AR.java b/lib/src/main/java/com/unity3d/services/ar/api/AR.java index ff074f0..4176898 100644 --- a/lib/src/main/java/com/unity3d/services/ar/api/AR.java +++ b/lib/src/main/java/com/unity3d/services/ar/api/AR.java @@ -135,6 +135,33 @@ public class AR { } } + @WebViewExposed + public static void swapBuffers(WebViewCallback callback) { + AdUnitActivity activity = AdUnit.getAdUnitActivity(); + if (activity != null && AR.getARView() != null) { + AR.getARView().swapBuffers(); + callback.invoke(); + } else { + callback.error(ARError.ARVIEW_NULL); + } + } + + @WebViewExposed + public static void setViewRenderMode(Integer mode, WebViewCallback callback) { + final AdUnitActivity activity = AdUnit.getAdUnitActivity(); + if (activity != null && AR.getARView() != null) { + try { + AR.getARView().setRenderMode(mode); + } catch (IllegalArgumentException e) { + callback.error(ARError.INVALID_VALUE); + return; + } + callback.invoke(); + } else { + callback.error(ARError.ARVIEW_NULL); + } + } + // Android doesn't support video format selection at the moment and it's not possible to get the // image size before session starts. This is here for API compatibility with iOS. @WebViewExposed diff --git a/lib/src/main/java/com/unity3d/services/ar/view/ARView.java b/lib/src/main/java/com/unity3d/services/ar/view/ARView.java index 0382a5f..638c1b6 100644 --- a/lib/src/main/java/com/unity3d/services/ar/view/ARView.java +++ b/lib/src/main/java/com/unity3d/services/ar/view/ARView.java @@ -3,7 +3,6 @@ package com.unity3d.services.ar.view; import android.annotation.TargetApi; import android.content.Context; import android.opengl.GLES20; -import android.opengl.GLSurfaceView; import android.os.Build; import android.util.SparseArray; @@ -79,7 +78,8 @@ public class ARView extends GLSurfaceView implements GLSurfaceView.Renderer { setEGLContextClientVersion(2); setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending. setRenderer(this); - setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + setPreserveEGLContextOnPause(true); _displayRotationHelper = new DisplayRotationHelper(context); } @@ -144,8 +144,6 @@ public class ARView extends GLSurfaceView implements GLSurfaceView.Renderer { @Override public void onDrawFrame(GL10 gl) { - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - if (_shouldSendResize) { // Fix for Samsung devices (S7-S9, manually send AR_WINDOW_RESIZED) WebViewApp webViewApp = WebViewApp.getCurrentApp(); @@ -176,22 +174,12 @@ public class ARView extends GLSurfaceView implements GLSurfaceView.Renderer { return; } - if (!_showCameraFrame) { - return; - } - _backgroundRenderer.draw(frame); - long currentTime = System.currentTimeMillis(); if (_timeOfLastDrawnCameraFrame == 0) { _timeOfLastDrawnCameraFrame = currentTime; } - long timeSinceLastDrawnCameraFrame = currentTime - _timeOfLastDrawnCameraFrame; - if (timeSinceLastDrawnCameraFrame < FRAME_UPDATE_TIMEOUT && !_drawNextCameraFrame) { - return; - } _timeOfLastDrawnCameraFrame = currentTime; - _drawNextCameraFrame = false; camera.getProjectionMatrix(_projectionMatrixArray, 0, _arNear, _arFar); camera.getViewMatrix(_viewMatrixArray, 0); @@ -234,6 +222,13 @@ public class ARView extends GLSurfaceView implements GLSurfaceView.Renderer { updatePlanes(updatedTrackables); DeviceLog.debug(frame.toString()); + + if (!_showCameraFrame) { + return; + } + + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + _backgroundRenderer.draw(frame); } } @@ -325,6 +320,13 @@ public class ARView extends GLSurfaceView implements GLSurfaceView.Renderer { _sessionRunning = true; _displayRotationHelper.onResume(); + + postDelayed(new Runnable() { + @Override + public void run() { + requestRender(); + } + }, 200); } public void pauseSession() { @@ -334,7 +336,7 @@ public class ARView extends GLSurfaceView implements GLSurfaceView.Renderer { } public void setDrawNextCameraFrame() { - _drawNextCameraFrame = true; + requestRender(); } public boolean getShowCameraFrame() { diff --git a/lib/src/main/java/com/unity3d/services/ar/view/GLSurfaceView.java b/lib/src/main/java/com/unity3d/services/ar/view/GLSurfaceView.java new file mode 100644 index 0000000..94ceedc --- /dev/null +++ b/lib/src/main/java/com/unity3d/services/ar/view/GLSurfaceView.java @@ -0,0 +1,1973 @@ +// Slightly modified GLSurfaceView +// We don't want to call eglSwapBuffers immediately after drawing. + +package com.unity3d.services.ar.view; + +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.Context; +import android.opengl.EGL14; +import android.opengl.EGLExt; +import android.opengl.GLDebugHelper; +import android.os.Trace; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +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; + +/** + * An implementation of SurfaceView that uses the dedicated surface for + * displaying OpenGL rendering. + *

+ * A GLSurfaceView provides the following features: + *

+ *

+ * + *
+ *

Developer Guides

+ *

For more information about how to use OpenGL, read the + * OpenGL developer guide.

+ *
+ * + *

Using GLSurfaceView

+ *

+ * Typically you use GLSurfaceView by subclassing it and overriding one or more of the + * View system input event methods. If your application does not need to override event + * methods then GLSurfaceView can be used as-is. For the most part + * GLSurfaceView behavior is customized by calling "set" methods rather than by subclassing. + * For example, unlike a regular View, drawing is delegated to a separate Renderer object which + * is registered with the GLSurfaceView + * using the {@link #setRenderer(GLSurfaceView.Renderer)} call. + *

+ *

Initializing GLSurfaceView

+ * All you have to do to initialize a GLSurfaceView is call {@link #setRenderer(GLSurfaceView.Renderer)}. + * However, if desired, you can modify the default behavior of GLSurfaceView by calling one or + * more of these methods before calling setRenderer: + * + *

+ *

Specifying the android.view.Surface

+ * By default GLSurfaceView will create a PixelFormat.RGB_888 format surface. If a translucent + * surface is required, call getHolder().setFormat(PixelFormat.TRANSLUCENT). + * The exact format of a TRANSLUCENT surface is device dependent, but it will be + * a 32-bit-per-pixel surface with 8 bits per component. + *

+ *

Choosing an EGL Configuration

+ * A given Android device may support multiple EGLConfig rendering configurations. + * The available configurations may differ in how many channels of data are present, as + * well as how many bits are allocated to each channel. Therefore, the first thing + * GLSurfaceView has to do when starting to render is choose what EGLConfig to use. + *

+ * By default GLSurfaceView chooses a EGLConfig that has an RGB_888 pixel format, + * with at least a 16-bit depth buffer and no stencil. + *

+ * If you would prefer a different EGLConfig + * you can override the default behavior by calling one of the + * setEGLConfigChooser methods. + *

+ *

Debug Behavior

+ * You can optionally modify the behavior of GLSurfaceView by calling + * one or more of the debugging methods {@link #setDebugFlags(int)}, + * and {@link #setGLWrapper}. These methods may be called before and/or after setRenderer, but + * typically they are called before setRenderer so that they take effect immediately. + *

+ *

Setting a Renderer

+ * Finally, you must call {@link #setRenderer} to register a {@link GLSurfaceView.Renderer}. + * The renderer is + * responsible for doing the actual OpenGL rendering. + *

+ *

Rendering Mode

+ * Once the renderer is set, you can control whether the renderer draws + * continuously or on-demand by calling + * {@link #setRenderMode}. The default is continuous rendering. + *

+ *

Activity Life-cycle

+ * A GLSurfaceView must be notified when to pause and resume rendering. GLSurfaceView clients + * are required to call {@link #onPause()} when the activity stops and + * {@link #onResume()} when the activity starts. These calls allow GLSurfaceView to + * pause and resume the rendering thread, and also allow GLSurfaceView to release and recreate + * the OpenGL display. + *

+ *

Handling events

+ *

+ * To handle an event you will typically subclass GLSurfaceView and override the + * appropriate method, just as you would with any other View. However, when handling + * the event, you may need to communicate with the Renderer object + * that's running in the rendering thread. You can do this using any + * standard Java cross-thread communication mechanism. In addition, + * one relatively easy way to communicate with your renderer is + * to call + * {@link #queueEvent(Runnable)}. For example: + *

+ * class MyGLSurfaceView extends GLSurfaceView {
+ *
+ *     private MyRenderer mMyRenderer;
+ *
+ *     public void start() {
+ *         mMyRenderer = ...;
+ *         setRenderer(mMyRenderer);
+ *     }
+ *
+ *     public boolean onKeyDown(int keyCode, KeyEvent event) {
+ *         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ *             queueEvent(new Runnable() {
+ *                 // This method will be called on the rendering
+ *                 // thread:
+ *                 public void run() {
+ *                     mMyRenderer.handleDpadCenter();
+ *                 }});
+ *             return true;
+ *         }
+ *         return super.onKeyDown(keyCode, event);
+ *     }
+ * }
+ * 
+ * + */ +public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback2 { + private final static String TAG = "GLSurfaceView"; + private final static boolean LOG_ATTACH_DETACH = false; + private final static boolean LOG_THREADS = false; + private final static boolean LOG_PAUSE_RESUME = false; + private final static boolean LOG_SURFACE = false; + private final static boolean LOG_RENDERER = false; + private final static boolean LOG_RENDERER_DRAW_FRAME = false; + private final static boolean LOG_EGL = false; + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + public final static int RENDERMODE_WHEN_DIRTY = 0; + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + public final static int RENDERMODE_CONTINUOUSLY = 1; + + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_CHECK_GL_ERROR = 1; + + /** + * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_LOG_GL_CALLS = 2; + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context) { + super(context); + init(); + } + + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public GLSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + + 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); + // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment + // this statement if back-porting to 2.2 or older: + // holder.setFormat(PixelFormat.RGB_565); + // + // setType is not needed for SDK 2.0 or newer. Uncomment this + // statement if back-porting this code to older SDKs. + // holder.setType(SurfaceHolder.SURFACE_TYPE_GPU); + } + + /** + * Set the glWrapper. If the glWrapper is not null, its + * {@link GLSurfaceView.GLWrapper#wrap(GL)} method is called + * whenever a surface is created. A GLWrapper can be used to wrap + * the GL object that's passed to the renderer. Wrapping a GL + * object enables examining and modifying the behavior of the + * GL calls made by the renderer. + *

+ * Wrapping is typically used for debugging purposes. + *

+ * The default value is null. + * @param glWrapper the new GLWrapper + */ + public void setGLWrapper(GLSurfaceView.GLWrapper glWrapper) { + mGLWrapper = glWrapper; + } + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + + /** + * Get the current value of the debug flags. + * @return the current value of the debug flags. + */ + public int getDebugFlags() { + return mDebugFlags; + } + + /** + * 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) { + mPreserveEGLContextOnPause = preserveOnPause; + } + + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + + /** + * 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: + *

+ *

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

+ * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(GLSurfaceView.Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + + /** + * Install a custom EGLContextFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(GLSurfaceView.Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + public void setEGLContextFactory(GLSurfaceView.EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + + /** + * Install a custom EGLWindowSurfaceFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(GLSurfaceView.Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + public void setEGLWindowSurfaceFactory(GLSurfaceView.EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + + /** + * Install a custom EGLConfigChooser. + *

If this method is + * called, it must be called before {@link #setRenderer(GLSurfaceView.Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + * @param configChooser + */ + public void setEGLConfigChooser(GLSurfaceView.EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + + /** + * Install a config chooser which will choose a config + * as close to 16-bit RGB as possible, with or without an optional depth + * buffer as close to 16-bits as possible. + *

If this method is + * called, it must be called before {@link #setRenderer(GLSurfaceView.Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + * @param needDepth + */ + public void setEGLConfigChooser(boolean needDepth) { + setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); + } + + /** + * Install a config chooser which will choose a config + * with at least the specified depthSize and stencilSize, + * and exactly the specified redSize, greenSize, blueSize and alphaSize. + *

If this method is + * called, it must be called before {@link #setRenderer(GLSurfaceView.Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + */ + public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, + blueSize, alphaSize, depthSize, stencilSize)); + } + + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + *

Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + *

+	 *     public MyView(Context context) {
+	 *         super(context);
+	 *         setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
+	 *         setRenderer(new MyRenderer());
+	 *     }
+	 * 
+ *

Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + *

If this method is called, it must be called before {@link #setRenderer(GLSurfaceView.Renderer)} + * is called. + *

This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(GLSurfaceView.EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(GLSurfaceView.EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + + /** + * 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 + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public void setRenderMode(int renderMode) { + mGLThread.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. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #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() { + mGLThread.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) { + mGLThread.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 + mGLThread.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) { + mGLThread.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 (mGLThread != null) { + mGLThread.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)}. + * + * This method should be called when it is no longer desirable for the + * GLSurfaceView to continue rendering, such as in response to + * {@link android.app.Activity#onStop Activity.onStop}. + * + * Must not be called before a renderer has been set. + */ + public void onPause() { + mGLThread.onPause(); + } + + /** + * Resumes the rendering thread, re-creating the OpenGL context if necessary. It + * is the counterpart to {@link #onPause()}. + * + * This method should typically be called in + * {@link android.app.Activity#onStart Activity.onStart}. + * + * Must not be called before a renderer has been set. + */ + public void onResume() { + mGLThread.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) { + mGLThread.queueEvent(r); + } + + public void swapBuffers() { + mGLThread.swapBuffers(); + } + + /** + * 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 (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + + // ---------------------------------------------------------------------- + + /** + * An interface used to wrap a GL interface. + *

Typically + * used for implementing debugging and tracing on top of the default + * GL interface. You would typically use this by creating your own class + * that implemented all the GL methods by delegating to another GL instance. + * Then you could add your own behavior before or after calling the + * delegate. All the GLWrapper would do was instantiate and return the + * wrapper GL instance: + *

+	 * class MyGLWrapper implements GLWrapper {
+	 *     GL wrap(GL gl) {
+	 *         return new MyGLImplementation(gl);
+	 *     }
+	 *     static class MyGLImplementation implements GL,GL10,GL11,... {
+	 *         ...
+	 *     }
+	 * }
+	 * 
+ * @see #setGLWrapper(GLSurfaceView.GLWrapper) + */ + public interface GLWrapper { + /** + * Wraps a gl interface in another gl interface. + * @param gl a GL interface that is to be wrapped. + * @return either the input argument or another GL object that wraps the input argument. + */ + GL wrap(GL gl); + } + + /** + * A generic renderer interface. + *

+ * The renderer is responsible for making OpenGL calls to render a frame. + *

+ * GLSurfaceView clients typically create their own classes that implement + * this interface, and then call {@link GLSurfaceView#setRenderer} to + * register the renderer with the GLSurfaceView. + *

+ * + *

+ *

Developer Guides

+ *

For more information about how to use OpenGL, read the + * OpenGL developer guide.

+ *
+ * + *

Threading

+ * The renderer will be called on a separate thread, so that rendering + * performance is decoupled from the UI thread. Clients typically need to + * communicate with the renderer from the UI thread, because that's where + * input events are received. Clients can communicate using any of the + * standard Java techniques for cross-thread communication, or they can + * use the {@link GLSurfaceView#queueEvent(Runnable)} convenience method. + *

+ *

EGL Context Lost

+ * There are situations where the EGL rendering context will be lost. This + * typically happens when device wakes up after going to sleep. When + * the EGL context is lost, all OpenGL resources (such as textures) that are + * associated with that context will be automatically deleted. In order to + * keep rendering correctly, a renderer must recreate any lost resources + * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method + * is a convenient place to do this. + * + * + * @see #setRenderer(GLSurfaceView.Renderer) + */ + public interface Renderer { + /** + * Called when the surface is created or recreated. + *

+ * Called when the rendering thread + * starts and whenever the EGL context is lost. The EGL context will typically + * be lost when the Android device awakes after going to sleep. + *

+ * Since this method is called at the beginning of rendering, as well as + * every time the EGL context is lost, this method is a convenient place to put + * code to create resources that need to be created when the rendering + * starts, and that need to be recreated when the EGL context is lost. + * Textures are an example of a resource that you might want to create + * here. + *

+ * Note that when the EGL context is lost, all OpenGL resources associated + * with that context will be automatically deleted. You do not need to call + * the corresponding "glDelete" methods such as glDeleteTextures to + * manually delete these lost resources. + *

+ * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + * @param config the EGLConfig of the created surface. Can be used + * to create matching pbuffers. + */ + void onSurfaceCreated(GL10 gl, EGLConfig config); + + /** + * Called when the surface changed size. + *

+ * Called after the surface is created and whenever + * the OpenGL ES surface size changes. + *

+ * Typically you will set your viewport here. If your camera + * is fixed then you could also set your projection matrix here: + *

+		 * void onSurfaceChanged(GL10 gl, int width, int height) {
+		 *     gl.glViewport(0, 0, width, height);
+		 *     // for a fixed camera, set the projection too
+		 *     float ratio = (float) width / height;
+		 *     gl.glMatrixMode(GL10.GL_PROJECTION);
+		 *     gl.glLoadIdentity();
+		 *     gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
+		 * }
+		 * 
+ * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + * @param width + * @param height + */ + void onSurfaceChanged(GL10 gl, int width, int height); + + /** + * Called to draw the current frame. + *

+ * This method is responsible for drawing the current frame. + *

+ * The implementation of this method typically looks like this: + *

+		 * void onDrawFrame(GL10 gl) {
+		 *     gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
+		 *     //... other gl calls to render the scene ...
+		 * }
+		 * 
+ * @param gl the GL interface. Use instanceof to + * test if the interface supports GL11 or higher interfaces. + */ + void onDrawFrame(GL10 gl); + } + + /** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLContextFactory(GLSurfaceView.EGLContextFactory)} + */ + public interface EGLContextFactory { + EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); + void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); + } + + private class DefaultContextFactory implements GLSurfaceView.EGLContextFactory { + private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, + EGL10.EGL_NONE }; + + return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attrib_list : null); + } + + public void destroyContext(EGL10 egl, EGLDisplay display, + EGLContext context) { + if (!egl.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (LOG_THREADS) { + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContex", egl.eglGetError()); + } + } + } + + /** + * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLWindowSurfaceFactory(GLSurfaceView.EGLWindowSurfaceFactory)} + */ + public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, + Object nativeWindow); + void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); + } + + private static class DefaultWindowSurfaceFactory implements GLSurfaceView.EGLWindowSurfaceFactory { + + public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, + EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = egl.eglCreateWindowSurface(display, config, nativeWindow, null); + } catch (IllegalArgumentException e) { + // 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(TAG, "eglCreateWindowSurface", e); + } + return result; + } + + public void destroySurface(EGL10 egl, EGLDisplay display, + EGLSurface surface) { + egl.eglDestroySurface(display, surface); + } + } + + /** + * An interface for choosing an EGLConfig configuration from a list of + * potential configurations. + *

+ * This interface must be implemented by clients wishing to call + * {@link GLSurfaceView#setEGLConfigChooser(GLSurfaceView.EGLConfigChooser)} + */ + public interface EGLConfigChooser { + /** + * Choose a configuration from the list. Implementors typically + * implement this method by calling + * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the + * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. + * @param egl the EGL10 for the current display. + * @param display the current display. + * @return the chosen configuration. + */ + EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); + } + + private abstract class BaseConfigChooser + implements GLSurfaceView.EGLConfigChooser { + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { + int[] num_config = new int[1]; + if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException( + "No configs match configSpec"); + } + + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, + num_config)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(egl, display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + + abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs); + + protected int[] mConfigSpec; + + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); + newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; + if (mEGLContextClientVersion == 2) { + newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */ + } else { + newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */ + } + newConfigSpec[len+1] = EGL10.EGL_NONE; + return newConfigSpec; + } + } + + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + private class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(new int[] { + EGL10.EGL_RED_SIZE, redSize, + EGL10.EGL_GREEN_SIZE, greenSize, + EGL10.EGL_BLUE_SIZE, blueSize, + EGL10.EGL_ALPHA_SIZE, alphaSize, + EGL10.EGL_DEPTH_SIZE, depthSize, + EGL10.EGL_STENCIL_SIZE, stencilSize, + EGL10.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + + @Override + public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, + EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, + EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, + EGL10.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + int r = findConfigAttrib(egl, display, config, + EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, + EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, + EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, + EGL10.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) + && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + + private int findConfigAttrib(EGL10 egl, EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } + + /** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + * + */ + private class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(boolean withDepthBuffer) { + super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } + } + + /** + * An EGL helper class. + */ + + private static class EglHelper { + public EglHelper(WeakReference glSurfaceViewWeakRef) { + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + public void start() { + if (LOG_EGL) { + Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); + } + /* + * 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) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if(!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = view.mEGLConfigChooser.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.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + if (LOG_EGL) { + Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); + } + + 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. + */ + public boolean createSurface() { + if (LOG_EGL) { + Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + mEglSurface = view.mEGLWindowSurfaceFactory.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("EglHelper", "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("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + * @return + */ + GL createGL() { + + GL gl = mEglContext.getGL(); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + if (view.mGLWrapper != null) { + gl = view.mGLWrapper.wrap(gl); + } + + if ((view.mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { + int configFlags = 0; + Writer log = null; + if ((view.mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { + configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; + } + if ((view.mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { + log = new LogWriter(); + } + gl = GLDebugHelper.wrap(gl, configFlags, log); + } + } + return gl; + } + + /** + * 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; + } + + public void destroySurface() { + if (LOG_EGL) { + Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); + } + 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); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + + public void finish() { + if (LOG_EGL) { + Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + if (LOG_THREADS) { + Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + + message); + } + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return ""; + } + + 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(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = RENDERMODE_CONTINUOUSLY; + mWantRenderNotification = false; + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + + @Override + public void run() { + setName("GLThread " + getId()); + if (LOG_THREADS) { + Log.i("GLThread", "starting tid=" + getId()); + } + + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + sGLThreadManager.threadExiting(this); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + sGLThreadManager.releaseEglContextLocked(this); + } + } + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mGLSurfaceViewWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + mWantRenderNotification = 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; + boolean shouldSwap = false; + int w = 0; + int h = 0; + Runnable event = null; + Runnable finishDrawingRunnable = null; + + while (true) { + synchronized (sGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + + if (! mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + sGLThreadManager.notifyAll(); + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); + } + } + + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + boolean preserveEglContextOnPause = view == null ? + false : view.mPreserveEGLContextOnPause; + if (!preserveEglContextOnPause) { + stopEglContextLocked(); + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); + } + } + } + + // Have we lost the SurfaceView surface? + if ((! mHasSurface) && (! mWaitingForSurface)) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + sGLThreadManager.notifyAll(); + } + + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + sGLThreadManager.notifyAll(); + } + + if (doRenderNotification) { + if (LOG_SURFACE) { + Log.i("GLThread", "sending render notification tid=" + getId()); + } + mWantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + sGLThreadManager.notifyAll(); + } + + if (mFinishDrawingRunnable != null) { + finishDrawingRunnable = mFinishDrawingRunnable; + mFinishDrawingRunnable = null; + } + + // Ready to draw? + if (readyToDraw()) { + + // If we don't have an EGL context, try to acquire one. + if (! mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + sGLThreadManager.releaseEglContextLocked(this); + throw t; + } + mHaveEglContext = true; + createEglContext = true; + + sGLThreadManager.notifyAll(); + } + } + + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + createGlInterface = true; + sizeChanged = true; + } + + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + mWantRenderNotification = true; + if (LOG_SURFACE) { + Log.i("GLThread", + "noticing that we want render notification tid=" + + getId()); + } + + // Destroy and recreate the EGL surface. + createEglSurface = true; + + mSizeChanged = false; + } + mRequestRender = false; + if (mRequestSwap) { + mRequestSwap = false; + shouldSwap = true; + } + sGLThreadManager.notifyAll(); + if (mWantRenderNotification) { + 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(). + if (LOG_THREADS) { + Log.i("GLThread", "waiting tid=" + getId() + + " mHaveEglContext: " + mHaveEglContext + + " mHaveEglSurface: " + mHaveEglSurface + + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + + " mPaused: " + mPaused + + " mHasSurface: " + mHasSurface + + " mSurfaceIsBad: " + mSurfaceIsBad + + " mWaitingForSurface: " + mWaitingForSurface + + " mWidth: " + mWidth + + " mHeight: " + mHeight + + " mRequestRender: " + mRequestRender + + " mRenderMode: " + mRenderMode); + } + sGLThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + + if (event != null) { + event.run(); + event = null; + continue; + } + + if (createEglSurface) { + if (LOG_SURFACE) { + Log.w("GLThread", "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + sGLThreadManager.notifyAll(); + } + } else { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + + if (shouldSwap && mDidDraw) { + mDidDraw = false; + shouldSwap = false; + int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + if (LOG_SURFACE) { + Log.i("GLThread", "egl context lost tid=" + getId()); + } + 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("GLThread", "eglSwapBuffers", swapError); + + synchronized (sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + + continue; + } + + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + + createGlInterface = false; + } + + if (createEglContext) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceCreated"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); + } + createEglContext = false; + } + + if (sizeChanged) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); + } + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onSurfaceChanged(gl, w, h); + } + sizeChanged = false; + } + + if (LOG_RENDERER_DRAW_FRAME) { + Log.w("GLThread", "onDrawFrame tid=" + getId()); + } + + { + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onDrawFrame(gl); + mDidDraw = true; + if (finishDrawingRunnable != null) { + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + } + + if (mRenderMode == RENDERMODE_CONTINUOUSLY) { + shouldSwap = false; + int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + if (LOG_SURFACE) { + Log.i("GLThread", "egl context lost tid=" + getId()); + } + 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("GLThread", "eglSwapBuffers", swapError); + + synchronized (sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + } + + if (wantRenderNotification) { + doRenderNotification = true; + wantRenderNotification = false; + } + } + + } finally { + /* + * clean-up everything... + */ + synchronized (sGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + + private boolean readyToDraw() { + return (!mPaused) && mHasSurface && (!mSurfaceIsBad) + && (mWidth > 0) && (mHeight > 0) + && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); + } + + public void setRenderMode(int renderMode) { + if ( !((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY)) ) { + throw new IllegalArgumentException("renderMode"); + } + synchronized(sGLThreadManager) { + mRenderMode = renderMode; + sGLThreadManager.notifyAll(); + } + } + + public int getRenderMode() { + synchronized(sGLThreadManager) { + return mRenderMode; + } + } + + public void requestRender() { + synchronized(sGLThreadManager) { + mRequestRender = true; + sGLThreadManager.notifyAll(); + } + } + + public void swapBuffers() { + synchronized (sGLThreadManager) { + mRequestSwap = true; + sGLThreadManager.notifyAll(); + } + } + + public void requestRenderAndNotify(Runnable finishDrawing) { + synchronized(sGLThreadManager) { + // 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; + } + + mWantRenderNotification = true; + mRequestRender = true; + mRenderComplete = false; + mFinishDrawingRunnable = finishDrawing; + + sGLThreadManager.notifyAll(); + } + } + + public void surfaceCreated() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + sGLThreadManager.notifyAll(); + while (mWaitingForSurface + && !mFinishedCreatingEglSurface + && !mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void surfaceDestroyed() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + sGLThreadManager.notifyAll(); + while((!mWaitingForSurface) && (!mExited)) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onPause() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onPause tid=" + getId()); + } + mRequestPaused = true; + sGLThreadManager.notifyAll(); + while ((! mExited) && (! mPaused)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onPause waiting for mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onResume() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + while ((! mExited) && mPaused && (!mRenderComplete)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onResume waiting for !mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void onWindowResize(int w, int h) { + synchronized (sGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = 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; + } + + sGLThreadManager.notifyAll(); + + // Wait for thread to react to resize and render a frame + while (! mExited && !mPaused && !mRenderComplete + && ableToDraw()) { + if (LOG_SURFACE) { + Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized(sGLThreadManager) { + mShouldExit = true; + sGLThreadManager.notifyAll(); + while (! mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + sGLThreadManager.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(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized(sGLThreadManager) { + mEventQueue.add(r); + sGLThreadManager.notifyAll(); + } + } + + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean mRequestSwap; + private boolean mShouldExit; + private boolean mDidDraw; + private boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private boolean mRequestRender; + private boolean mWantRenderNotification; + private boolean mRenderComplete; + private ArrayList mEventQueue = new ArrayList(); + private boolean mSizeChanged = true; + private Runnable mFinishDrawingRunnable = null; + + // End of member variables protected by the sGLThreadManager monitor. + + private EglHelper mEglHelper; + + /** + * 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 (mGLThread != null) { + throw new IllegalStateException( + "setRenderer has already been called for this instance."); + } + } + + private static class GLThreadManager { + private static String TAG = "GLThreadManager"; + + public synchronized void threadExiting(GLThread thread) { + if (LOG_THREADS) { + Log.i("GLThread", "exiting tid=" + thread.getId()); + } + thread.mExited = true; + notifyAll(); + } + + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + notifyAll(); + } + } + + private static final GLThreadManager sGLThreadManager = new GLThreadManager(); + + private final WeakReference mThisWeakRef = + new WeakReference<>(this); + private GLThread mGLThread; + private GLSurfaceView.Renderer mRenderer; + private boolean mDetached; + private GLSurfaceView.EGLConfigChooser mEGLConfigChooser; + private GLSurfaceView.EGLContextFactory mEGLContextFactory; + private GLSurfaceView.EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private GLSurfaceView.GLWrapper mGLWrapper; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} diff --git a/lib/src/main/java/com/unity3d/services/banners/UnityBanners.java b/lib/src/main/java/com/unity3d/services/banners/UnityBanners.java index 226d5fb..6d079ac 100644 --- a/lib/src/main/java/com/unity3d/services/banners/UnityBanners.java +++ b/lib/src/main/java/com/unity3d/services/banners/UnityBanners.java @@ -4,6 +4,7 @@ import android.app.Activity; import com.unity3d.ads.UnityAds; import com.unity3d.services.ads.placement.Placement; +import com.unity3d.services.banners.view.BannerPosition; import com.unity3d.services.core.log.DeviceLog; import com.unity3d.services.core.misc.Utilities; import com.unity3d.services.core.properties.ClientProperties; @@ -64,6 +65,10 @@ public final class UnityBanners { }).start(); } + public static void setBannerPosition(BannerPosition position) { + ClientProperties.setBannerDefaultPosition(position); + } + /** * Change listener for IUnityAdsListener callbacks * diff --git a/lib/src/main/java/com/unity3d/services/banners/api/Banner.java b/lib/src/main/java/com/unity3d/services/banners/api/Banner.java index fd7d860..d07704c 100644 --- a/lib/src/main/java/com/unity3d/services/banners/api/Banner.java +++ b/lib/src/main/java/com/unity3d/services/banners/api/Banner.java @@ -30,9 +30,14 @@ public class Banner { @Override public synchronized void run() { BannerView view = BannerView.getOrCreateInstance(); - view.setBannerDimensions(width, height, BannerPosition.fromString(style)); - view.setViews(getArrayFromJSONArray(viewsArray)); + BannerPosition position = ClientProperties.getbannerDefaultPosition(); + if (position == null) { + position = BannerPosition.fromString(style); + } + + view.setBannerDimensions(width, height, position); + view.setViews(getArrayFromJSONArray(viewsArray)); WebViewApp app = WebViewApp.getCurrentApp(); if (app != null) { app.sendEvent(WebViewEventCategory.BANNER, BannerEvent.BANNER_LOADED); @@ -104,7 +109,12 @@ public class Banner { public void run() { BannerView view = BannerView.getInstance(); if (view != null) { - view.setBannerDimensions(width, height, BannerPosition.fromString(style)); + BannerPosition position = ClientProperties.getbannerDefaultPosition(); + if ( position == null) { + position = BannerPosition.fromString(style); + } + view.setBannerDimensions(width, height, position); + view.setLayoutParams(view.getLayoutParams()); } } diff --git a/lib/src/main/java/com/unity3d/services/banners/view/BannerPosition.java b/lib/src/main/java/com/unity3d/services/banners/view/BannerPosition.java index a641841..85c3c7f 100644 --- a/lib/src/main/java/com/unity3d/services/banners/view/BannerPosition.java +++ b/lib/src/main/java/com/unity3d/services/banners/view/BannerPosition.java @@ -4,6 +4,8 @@ import android.view.Gravity; import android.view.ViewGroup; import android.widget.RelativeLayout; +import com.unity3d.services.banners.UnityBanners; + public enum BannerPosition { TOP_LEFT(new int[] {RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.ALIGN_PARENT_LEFT}, Gravity.TOP | Gravity.LEFT), TOP_CENTER(new int[] {RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.CENTER_HORIZONTAL}, Gravity.TOP | Gravity.CENTER_HORIZONTAL), diff --git a/lib/src/main/java/com/unity3d/services/core/api/Intent.java b/lib/src/main/java/com/unity3d/services/core/api/Intent.java index 812738f..5bc9bd9 100644 --- a/lib/src/main/java/com/unity3d/services/core/api/Intent.java +++ b/lib/src/main/java/com/unity3d/services/core/api/Intent.java @@ -56,11 +56,13 @@ public class Intent { if (action != null) intent.setAction(action); - if (uri != null) + if (uri != null && mimeType != null) { + intent.setDataAndType(Uri.parse(uri), mimeType); + } else if (uri != null) { intent.setData(Uri.parse(uri)); - - if (mimeType != null) + } else if (mimeType != null) { intent.setType(mimeType); + } if (flags != null && flags > -1) intent.setFlags(flags); diff --git a/lib/src/main/java/com/unity3d/services/core/api/Storage.java b/lib/src/main/java/com/unity3d/services/core/api/Storage.java index d55c7f9..79b6208 100644 --- a/lib/src/main/java/com/unity3d/services/core/api/Storage.java +++ b/lib/src/main/java/com/unity3d/services/core/api/Storage.java @@ -52,13 +52,13 @@ public class Storage { if (s != null) { boolean success = s.set(key, value); if (success) { - callback.invoke(key, value); + callback.invoke(key); } else { - callback.error(StorageError.COULDNT_SET_VALUE, key, value); + callback.error(StorageError.COULDNT_SET_VALUE, key); } } else { - callback.error(StorageError.COULDNT_GET_STORAGE, type, key, value); + callback.error(StorageError.COULDNT_GET_STORAGE, type, key); } } diff --git a/lib/src/main/java/com/unity3d/services/core/cache/CacheDirectory.java b/lib/src/main/java/com/unity3d/services/core/cache/CacheDirectory.java index 5230c0c..f577065 100644 --- a/lib/src/main/java/com/unity3d/services/core/cache/CacheDirectory.java +++ b/lib/src/main/java/com/unity3d/services/core/cache/CacheDirectory.java @@ -39,6 +39,8 @@ public class CacheDirectory { } if(testCacheDirectory(externalCache)) { + createNoMediaFile(externalCache); + _cacheDirectory = externalCache; _type = CacheDirectoryType.EXTERNAL; DeviceLog.debug("Unity Ads is using external cache directory: " + externalCache.getAbsolutePath()); @@ -124,4 +126,20 @@ public class CacheDirectory { return false; } } + + private void createNoMediaFile(File path) { + File noMediaFile = new File(path, ".nomedia"); + + try { + boolean created = noMediaFile.createNewFile(); + + if(created) { + DeviceLog.debug("Successfully created .nomedia file"); + } else { + DeviceLog.debug("Using existing .nomedia file"); + } + } catch(Exception e) { + DeviceLog.exception("Failed to create .nomedia file", e); + } + } } \ No newline at end of file diff --git a/lib/src/main/java/com/unity3d/services/core/configuration/Configuration.java b/lib/src/main/java/com/unity3d/services/core/configuration/Configuration.java index aaeda2d..eb5cb36 100644 --- a/lib/src/main/java/com/unity3d/services/core/configuration/Configuration.java +++ b/lib/src/main/java/com/unity3d/services/core/configuration/Configuration.java @@ -121,7 +121,7 @@ public class Configuration { return queryString; } - protected void makeRequest () throws IOException, JSONException, IllegalStateException, NetworkIOException { + protected void makeRequest () throws IOException, JSONException, IllegalStateException, NetworkIOException, IllegalArgumentException { if (_url == null) { throw new MalformedURLException("Base URL is null"); } diff --git a/lib/src/main/java/com/unity3d/services/core/device/Storage.java b/lib/src/main/java/com/unity3d/services/core/device/Storage.java index 369c1d2..2b9e793 100644 --- a/lib/src/main/java/com/unity3d/services/core/device/Storage.java +++ b/lib/src/main/java/com/unity3d/services/core/device/Storage.java @@ -25,21 +25,20 @@ public class Storage extends JsonStorage { public synchronized boolean readStorage () { File f = new File(_targetFileName); - String fileData = Utilities.readFile(f); - if (fileData != null) { - try { - setData(new JSONObject(Utilities.readFile(f))); - } - catch (Exception e) { - DeviceLog.exception("Error creating storage JSON", e); + try { + byte[] bytes = Utilities.readFileBytes(f); + if (bytes == null) { return false; } - + String fileData = new String(bytes); + setData(new JSONObject(fileData)); return true; } - - return false; + catch (Exception e) { + DeviceLog.exception("Error creating storage JSON", e); + return false; + } } public synchronized boolean initStorage () { diff --git a/lib/src/main/java/com/unity3d/services/core/misc/Utilities.java b/lib/src/main/java/com/unity3d/services/core/misc/Utilities.java index d2d25c3..6b19085 100644 --- a/lib/src/main/java/com/unity3d/services/core/misc/Utilities.java +++ b/lib/src/main/java/com/unity3d/services/core/misc/Utilities.java @@ -130,55 +130,6 @@ public class Utilities { return success; } - public static String readFile (File fileToRead) { - if (fileToRead == null) { - return null; - } - - String fileContent = ""; - BufferedReader br = null; - FileReader fr = null; - - if (fileToRead.exists() && fileToRead.canRead()) { - try { - fr = new FileReader(fileToRead); - br = new BufferedReader(fr); - String line; - - while ((line = br.readLine()) != null) { - fileContent = fileContent.concat(line); - } - } - catch (Exception e) { - DeviceLog.exception("Problem reading file", e); - fileContent = null; - } - try { - if (br != null) { - br.close(); - } - } - catch (Exception e) { - DeviceLog.exception("Couldn't close BufferedReader", e); - } - try { - if (fr != null) { - fr.close(); - } - } - catch (Exception e) { - DeviceLog.exception("Couldn't close FileReader", e); - } - - return fileContent; - } - else { - DeviceLog.error("File did not exist or couldn't be read"); - } - - return null; - } - public static byte[] readFileBytes(File file) throws IOException { if (file == null) { return null; diff --git a/lib/src/main/java/com/unity3d/services/core/properties/ClientProperties.java b/lib/src/main/java/com/unity3d/services/core/properties/ClientProperties.java index 81e827d..8e106c2 100644 --- a/lib/src/main/java/com/unity3d/services/core/properties/ClientProperties.java +++ b/lib/src/main/java/com/unity3d/services/core/properties/ClientProperties.java @@ -8,6 +8,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; +import com.unity3d.services.banners.view.BannerPosition; import com.unity3d.services.core.log.DeviceLog; import java.io.ByteArrayInputStream; @@ -25,6 +26,7 @@ public class ClientProperties { private static Application _application; private static String _gameId; + private static BannerPosition _bannerDefaultPosition; public static Activity getActivity () { return _activity.get(); @@ -74,6 +76,14 @@ public class ClientProperties { } } + public static BannerPosition getbannerDefaultPosition () { + return _bannerDefaultPosition; + } + + public static void setBannerDefaultPosition(BannerPosition position) { + _bannerDefaultPosition = position; + } + public static boolean isAppDebuggable() { boolean debuggable = false; boolean couldNotGetApplicationInfo = false; diff --git a/lib/src/main/java/com/unity3d/services/core/request/WebRequest.java b/lib/src/main/java/com/unity3d/services/core/request/WebRequest.java index a65a5cd..7606b9e 100644 --- a/lib/src/main/java/com/unity3d/services/core/request/WebRequest.java +++ b/lib/src/main/java/com/unity3d/services/core/request/WebRequest.java @@ -209,13 +209,13 @@ public class WebRequest { return total; } - public String makeRequest () throws NetworkIOException, IOException, IllegalStateException { + public String makeRequest () throws NetworkIOException, IOException, IllegalStateException, IllegalArgumentException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); makeStreamRequest(baos); return baos.toString("UTF-8"); } - private HttpURLConnection getHttpUrlConnectionWithHeaders() throws NetworkIOException { + private HttpURLConnection getHttpUrlConnectionWithHeaders() throws NetworkIOException, IllegalArgumentException { HttpURLConnection connection; if (getUrl().toString().startsWith("https://")) { @@ -226,7 +226,7 @@ public class WebRequest { throw new NetworkIOException("Open HTTPS connection: " + e.getMessage()); } } - else { + else if (getUrl().toString().startsWith("http://")) { try { connection = (HttpURLConnection)getUrl().openConnection(); } @@ -234,6 +234,9 @@ public class WebRequest { throw new NetworkIOException("Open HTTP connection: " + e.getMessage()); } } + else { + throw new IllegalArgumentException("Invalid url-protocol in url: " + getUrl().toString()); + } connection.setInstanceFollowRedirects(false); connection.setConnectTimeout(getConnectTimeout()); diff --git a/lib/src/main/java/com/unity3d/services/core/request/WebRequestRunnable.java b/lib/src/main/java/com/unity3d/services/core/request/WebRequestRunnable.java index 053f004..6e761f4 100644 --- a/lib/src/main/java/com/unity3d/services/core/request/WebRequestRunnable.java +++ b/lib/src/main/java/com/unity3d/services/core/request/WebRequestRunnable.java @@ -66,7 +66,7 @@ public class WebRequestRunnable implements Runnable { String response; try { response = _currentRequest.makeRequest(); - } catch (IOException | NetworkIOException | IllegalStateException e) { + } catch (IOException | NetworkIOException | IllegalStateException | IllegalArgumentException e) { DeviceLog.exception("Error completing request", e); onFailed(e.getClass().getName() + ": " + e.getMessage()); return; diff --git a/lib/src/main/java/com/unity3d/services/monetization/placementcontent/purchasing/PromoMetadataUtilities.java b/lib/src/main/java/com/unity3d/services/monetization/placementcontent/purchasing/PromoMetadataUtilities.java index 8a69b59..e25c9e5 100644 --- a/lib/src/main/java/com/unity3d/services/monetization/placementcontent/purchasing/PromoMetadataUtilities.java +++ b/lib/src/main/java/com/unity3d/services/monetization/placementcontent/purchasing/PromoMetadataUtilities.java @@ -78,7 +78,7 @@ public class PromoMetadataUtilities { productBuilder.withLocalizedTitle((String) productParams.get("localizedTitle")); } if (productParams.containsKey("localizedPrice")) { - productBuilder.withLocalizedPrice((double) productParams.get("localizedPrice")); + productBuilder.withLocalizedPrice(new Double(productParams.get("localizedPrice").toString())); } if (productParams.containsKey("productType")) { productBuilder.withProductType((String) productParams.get("productType")); diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..6ab8604 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -x + +echo "Building is done in downstream jobs!" diff --git a/scripts/extra.groovy b/scripts/extra.groovy new file mode 100644 index 0000000..2e25d4a --- /dev/null +++ b/scripts/extra.groovy @@ -0,0 +1,63 @@ +def main() { + // commitId value is not used, but is here for learning purposes + commitId = sh(returnStdout: true, script: 'git rev-parse HEAD').trim() + echo "#### Commit $commitId ####" + + if (env.BRANCH_NAME =~ /^PR-/) { + stage('checkout') { + dir('sharedLibs') { + checkout( + [$class: 'GitSCM', branches: [[name: 'master']], + userRemoteConfigs: [[credentialsId: 'applifier-readonly-jenkins-bot', + url: 'https://github.com/Applifier/unity-ads-sdk-tests.git']]] + ) + script { + sharedLibs = load 'sharedLibs.groovy' + } + } + } + + stage('Run tests') { + dir('results') { + parallel ( + 'hybrid_test': { + def jobName = "ads-sdk-hybrid-test-android" + def build_ = build( + job: "Applifier/unity-ads-sdk-tests/$jobName", + propagate: false, + wait: true, + parameters: [ + string(name: 'UNITY_ADS_ANDROID_BRANCH', value: env.CHANGE_BRANCH), + ] + ) + + def artifactFolder = "$jobName/$build_.number" + dir(jobName) { + sharedLibs.downloadFromGcp("$artifactFolder/*") + } + sharedLibs.removeFromGcp(artifactFolder) + }, + + 'system_test': { + def jobName = "ads-sdk-systest-android" + build( + job: "Applifier/unity-ads-sdk-tests/$jobName", + propagate: false, + wait: false, + parameters: [ + string(name: 'UNITY_ADS_ANDROID_BRANCH', value: env.CHANGE_BRANCH) + ], + ) + } + ) + } + archiveArtifacts artifacts: "results/**", fingerprint: true + step ([$class: "JUnitResultArchiver", testResults: "results/**/*.xml"]) + + slackChannel = "ads-sdk-notify" + sharedLibs.sendTestSummary(slackChannel) + } + } +} + +return this; diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..3e03e9b --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -x + +echo "Testing is done in downstream jobs!"