1/*
2 * Copyright (c) 2003-2009 jMonkeyEngine
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 * * Redistributions of source code must retain the above copyright
10 *   notice, this list of conditions and the following disclaimer.
11 *
12 * * Redistributions in binary form must reproduce the above copyright
13 *   notice, this list of conditions and the following disclaimer in the
14 *   documentation and/or other materials provided with the distribution.
15 *
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 *   may be used to endorse or promote products derived from this software
18 *   without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32package com.jme3.system.android;
33
34import android.app.Activity;
35import android.app.AlertDialog;
36import android.content.Context;
37import android.content.DialogInterface;
38import android.opengl.GLSurfaceView;
39import android.text.InputType;
40import android.view.Gravity;
41import android.view.SurfaceHolder;
42import android.view.ViewGroup.LayoutParams;
43import android.widget.EditText;
44import android.widget.FrameLayout;
45import com.jme3.app.AndroidHarness;
46import com.jme3.app.Application;
47import com.jme3.input.JoyInput;
48import com.jme3.input.KeyInput;
49import com.jme3.input.MouseInput;
50import com.jme3.input.SoftTextDialogInput;
51import com.jme3.input.TouchInput;
52import com.jme3.input.android.AndroidInput;
53import com.jme3.input.controls.SoftTextDialogInputListener;
54import com.jme3.input.controls.TouchTrigger;
55import com.jme3.input.dummy.DummyKeyInput;
56import com.jme3.input.dummy.DummyMouseInput;
57import com.jme3.renderer.android.OGLESShaderRenderer;
58import com.jme3.system.AppSettings;
59import com.jme3.system.JmeContext;
60import com.jme3.system.JmeSystem;
61import com.jme3.system.SystemListener;
62import com.jme3.system.Timer;
63import com.jme3.system.android.AndroidConfigChooser.ConfigType;
64import java.util.concurrent.atomic.AtomicBoolean;
65import java.util.logging.Level;
66import java.util.logging.Logger;
67import javax.microedition.khronos.egl.EGL10;
68import javax.microedition.khronos.egl.EGLConfig;
69import javax.microedition.khronos.egl.EGLContext;
70import javax.microedition.khronos.egl.EGLDisplay;
71import javax.microedition.khronos.opengles.GL10;
72
73public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTextDialogInput {
74
75    private static final Logger logger = Logger.getLogger(OGLESContext.class.getName());
76    protected final AtomicBoolean created = new AtomicBoolean(false);
77    protected final AtomicBoolean renderable = new AtomicBoolean(false);
78    protected final AtomicBoolean needClose = new AtomicBoolean(false);
79    protected final AppSettings settings = new AppSettings(true);
80
81    /*
82     * >= OpenGL ES 2.0 (Android 2.2+)
83     */
84    protected OGLESShaderRenderer renderer;
85    protected Timer timer;
86    protected SystemListener listener;
87    protected boolean autoFlush = true;
88    protected AndroidInput view;
89    private boolean firstDrawFrame = true;
90    //protected int minFrameDuration = 1000 / frameRate;  // Set a max FPS of 33
91    protected int minFrameDuration = 0;                   // No FPS cap
92    /**
93     * EGL_RENDERABLE_TYPE: EGL_OPENGL_ES_BIT = OpenGL ES 1.0 |
94     * EGL_OPENGL_ES2_BIT = OpenGL ES 2.0
95     */
96    protected int clientOpenGLESVersion = 1;
97    protected boolean verboseLogging = false;
98    final private String ESCAPE_EVENT = "TouchEscape";
99
100    public OGLESContext() {
101    }
102
103    @Override
104    public Type getType() {
105        return Type.Display;
106    }
107
108    /**
109     * <code>createView</code>
110     *
111     * @param activity The Android activity which is parent for the
112     * GLSurfaceView
113     * @return GLSurfaceView The newly created view
114     */
115    public GLSurfaceView createView(Activity activity) {
116        return createView(new AndroidInput(activity));
117    }
118
119    /**
120     * <code>createView</code>
121     *
122     * @param view The Android input which will be used as the GLSurfaceView for
123     * this context
124     * @return GLSurfaceView The newly created view
125     */
126    public GLSurfaceView createView(AndroidInput view) {
127        return createView(view, ConfigType.FASTEST, false);
128    }
129
130    /**
131     * <code>createView</code> initializes the GLSurfaceView
132     *
133     * @param view The Android input which will be used as the GLSurfaceView for
134     * this context
135     * @param configType ConfigType.FASTEST (Default) | ConfigType.LEGACY |
136     * ConfigType.BEST
137     * @param eglConfigVerboseLogging if true show all found configs
138     * @return GLSurfaceView The newly created view
139     */
140    public GLSurfaceView createView(AndroidInput view, ConfigType configType, boolean eglConfigVerboseLogging) {
141        // Start to set up the view
142        this.view = view;
143        verboseLogging = eglConfigVerboseLogging;
144
145        if (configType == ConfigType.LEGACY) {
146            // Hardcoded egl setup
147            clientOpenGLESVersion = 2;
148            view.setEGLContextClientVersion(2);
149            //RGB565, Depth16
150            view.setEGLConfigChooser(5, 6, 5, 0, 16, 0);
151            logger.info("ConfigType.LEGACY using RGB565");
152        } else {
153            EGL10 egl = (EGL10) EGLContext.getEGL();
154            EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
155
156            int[] version = new int[2];
157            if (egl.eglInitialize(display, version) == true) {
158                logger.log(Level.INFO, "Display EGL Version: {0}.{1}", new Object[]{version[0], version[1]});
159            }
160
161            try {
162                // Create a config chooser
163                AndroidConfigChooser configChooser = new AndroidConfigChooser(configType);
164                // Init chooser
165                if (!configChooser.findConfig(egl, display)) {
166                    listener.handleError("Unable to find suitable EGL config", null);
167                    return null;
168                }
169
170                clientOpenGLESVersion = configChooser.getClientOpenGLESVersion();
171                if (clientOpenGLESVersion < 2) {
172                    listener.handleError("OpenGL ES 2.0 is not supported on this device", null);
173                    return null;
174                }
175
176                // Requesting client version from GLSurfaceView which is extended by
177                // AndroidInput.
178                view.setEGLContextClientVersion(clientOpenGLESVersion);
179                view.setEGLConfigChooser(configChooser);
180                view.getHolder().setFormat(configChooser.getPixelFormat());
181            } finally {
182                if (display != null) {
183                    egl.eglTerminate(display);
184                }
185            }
186        }
187
188        view.setFocusableInTouchMode(true);
189        view.setFocusable(true);
190        view.getHolder().setType(SurfaceHolder.SURFACE_TYPE_GPU);
191        view.setRenderer(this);
192
193        return view;
194    }
195
196    // renderer:initialize
197    @Override
198    public void onSurfaceCreated(GL10 gl, EGLConfig cfg) {
199
200        if (created.get() && renderer != null) {
201            renderer.resetGLObjects();
202        } else {
203            if (!created.get()) {
204                logger.info("GL Surface created, doing JME3 init");
205                initInThread();
206            } else {
207                logger.warning("GL Surface already created");
208            }
209        }
210    }
211
212    protected void initInThread() {
213        created.set(true);
214
215        logger.info("OGLESContext create");
216        logger.info("Running on thread: " + Thread.currentThread().getName());
217
218        final Context ctx = this.view.getContext();
219
220        // Setup unhandled Exception Handler
221        if (ctx instanceof AndroidHarness) {
222            Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
223
224                public void uncaughtException(Thread thread, Throwable thrown) {
225                    ((AndroidHarness) ctx).handleError("Exception thrown in " + thread.toString(), thrown);
226                }
227            });
228        } else {
229            Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
230
231                public void uncaughtException(Thread thread, Throwable thrown) {
232                    listener.handleError("Exception thrown in " + thread.toString(), thrown);
233                }
234            });
235        }
236
237        if (clientOpenGLESVersion < 2) {
238            throw new UnsupportedOperationException("OpenGL ES 2.0 is not supported on this device");
239        }
240
241        timer = new AndroidTimer();
242        renderer = new OGLESShaderRenderer();
243
244        renderer.setUseVA(true);
245        renderer.setVerboseLogging(verboseLogging);
246
247        renderer.initialize();
248        listener.initialize();
249
250        // Setup exit hook
251        if (ctx instanceof AndroidHarness) {
252            Application app = ((AndroidHarness) ctx).getJmeApplication();
253            if (app.getInputManager() != null) {
254                app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
255                app.getInputManager().addListener((AndroidHarness) ctx, new String[]{ESCAPE_EVENT});
256            }
257        }
258
259        JmeSystem.setSoftTextDialogInput(this);
260
261        needClose.set(false);
262        renderable.set(true);
263    }
264
265    /**
266     * De-initialize in the OpenGL thread.
267     */
268    protected void deinitInThread() {
269        if (renderable.get()) {
270            created.set(false);
271            if (renderer != null) {
272                renderer.cleanup();
273            }
274
275            listener.destroy();
276
277            listener = null;
278            renderer = null;
279            timer = null;
280
281            // do android specific cleaning here
282            logger.info("Display destroyed.");
283
284            renderable.set(false);
285            final Context ctx = this.view.getContext();
286            if (ctx instanceof AndroidHarness) {
287                AndroidHarness harness = (AndroidHarness) ctx;
288                if (harness.isFinishOnAppStop()) {
289                    harness.finish();
290                }
291            }
292        }
293    }
294
295    protected void applySettingsToRenderer(OGLESShaderRenderer renderer, AppSettings settings) {
296        logger.warning("setSettings.USE_VA: [" + settings.getBoolean("USE_VA") + "]");
297        logger.warning("setSettings.VERBOSE_LOGGING: [" + settings.getBoolean("VERBOSE_LOGGING") + "]");
298        renderer.setUseVA(settings.getBoolean("USE_VA"));
299        renderer.setVerboseLogging(settings.getBoolean("VERBOSE_LOGGING"));
300    }
301
302    protected void applySettings(AppSettings settings) {
303        setSettings(settings);
304        if (renderer != null) {
305            applySettingsToRenderer(renderer, this.settings);
306        }
307    }
308
309    @Override
310    public void setSettings(AppSettings settings) {
311        this.settings.copyFrom(settings);
312    }
313
314    @Override
315    public void setSystemListener(SystemListener listener) {
316        this.listener = listener;
317    }
318
319    @Override
320    public AppSettings getSettings() {
321        return settings;
322    }
323
324    @Override
325    public com.jme3.renderer.Renderer getRenderer() {
326        return renderer;
327    }
328
329    @Override
330    public MouseInput getMouseInput() {
331        return new DummyMouseInput();
332    }
333
334    @Override
335    public KeyInput getKeyInput() {
336        return new DummyKeyInput();
337    }
338
339    @Override
340    public JoyInput getJoyInput() {
341        return null;
342    }
343
344    @Override
345    public TouchInput getTouchInput() {
346        return view;
347    }
348
349    @Override
350    public Timer getTimer() {
351        return timer;
352    }
353
354    @Override
355    public void setTitle(String title) {
356    }
357
358    @Override
359    public boolean isCreated() {
360        return created.get();
361    }
362
363    @Override
364    public void setAutoFlushFrames(boolean enabled) {
365        this.autoFlush = enabled;
366    }
367
368    // SystemListener:reshape
369    @Override
370    public void onSurfaceChanged(GL10 gl, int width, int height) {
371        logger.info("GL Surface changed, width: " + width + " height: " + height);
372        settings.setResolution(width, height);
373        listener.reshape(width, height);
374    }
375
376    // SystemListener:update
377    @Override
378    public void onDrawFrame(GL10 gl) {
379        if (needClose.get()) {
380            deinitInThread();
381            return;
382        }
383
384        if (renderable.get()) {
385            if (!created.get()) {
386                throw new IllegalStateException("onDrawFrame without create");
387            }
388
389            long milliStart = System.currentTimeMillis();
390
391            listener.update();
392
393            // call to AndroidHarness to remove the splash screen, if present.
394            // call after listener.update() to make sure no gap between
395            //   splash screen going away and app display being shown.
396            if (firstDrawFrame) {
397                final Context ctx = this.view.getContext();
398                if (ctx instanceof AndroidHarness) {
399                    ((AndroidHarness) ctx).removeSplashScreen();
400                }
401                firstDrawFrame = false;
402            }
403
404            if (autoFlush) {
405                renderer.onFrame();
406            }
407
408            long milliDelta = System.currentTimeMillis() - milliStart;
409
410            // Enforce a FPS cap
411            if (milliDelta < minFrameDuration) {
412                //logger.log(Level.INFO, "Time per frame {0}", milliDelta);
413                try {
414                    Thread.sleep(minFrameDuration - milliDelta);
415                } catch (InterruptedException e) {
416                }
417            }
418
419        }
420    }
421
422    @Override
423    public boolean isRenderable() {
424        return renderable.get();
425    }
426
427    @Override
428    public void create(boolean waitFor) {
429        if (waitFor) {
430            waitFor(true);
431        }
432    }
433
434    public void create() {
435        create(false);
436    }
437
438    @Override
439    public void restart() {
440    }
441
442    @Override
443    public void destroy(boolean waitFor) {
444        needClose.set(true);
445        if (waitFor) {
446            waitFor(false);
447        }
448    }
449
450    public void destroy() {
451        destroy(true);
452    }
453
454    protected void waitFor(boolean createdVal) {
455        while (renderable.get() != createdVal) {
456            try {
457                Thread.sleep(10);
458            } catch (InterruptedException ex) {
459            }
460        }
461    }
462
463    public int getClientOpenGLESVersion() {
464        return clientOpenGLESVersion;
465    }
466
467    public void requestDialog(final int id, final String title, final String initialValue, final SoftTextDialogInputListener listener) {
468        logger.log(Level.INFO, "requestDialog: title: {0}, initialValue: {1}",
469                new Object[]{title, initialValue});
470
471        JmeAndroidSystem.getActivity().runOnUiThread(new Runnable() {
472
473            @Override
474            public void run() {
475
476                final FrameLayout layoutTextDialogInput = new FrameLayout(JmeAndroidSystem.getActivity());
477                final EditText editTextDialogInput = new EditText(JmeAndroidSystem.getActivity());
478                editTextDialogInput.setWidth(LayoutParams.FILL_PARENT);
479                editTextDialogInput.setHeight(LayoutParams.FILL_PARENT);
480                editTextDialogInput.setPadding(20, 20, 20, 20);
481                editTextDialogInput.setGravity(Gravity.FILL_HORIZONTAL);
482
483                editTextDialogInput.setText(initialValue);
484
485                switch (id) {
486                    case SoftTextDialogInput.TEXT_ENTRY_DIALOG:
487
488                        editTextDialogInput.setInputType(InputType.TYPE_CLASS_TEXT);
489                        break;
490
491                    case SoftTextDialogInput.NUMERIC_ENTRY_DIALOG:
492
493                        editTextDialogInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED);
494                        break;
495
496                    case SoftTextDialogInput.NUMERIC_KEYPAD_DIALOG:
497
498                        editTextDialogInput.setInputType(InputType.TYPE_CLASS_PHONE);
499                        break;
500
501                    default:
502                        break;
503                }
504
505                layoutTextDialogInput.addView(editTextDialogInput);
506
507                AlertDialog dialogTextInput = new AlertDialog.Builder(JmeAndroidSystem.getActivity()).setTitle(title).setView(layoutTextDialogInput).setPositiveButton("OK",
508                        new DialogInterface.OnClickListener() {
509
510                            public void onClick(DialogInterface dialog, int whichButton) {
511                                /* User clicked OK, send COMPLETE action
512                                 * and text */
513                                listener.onSoftText(SoftTextDialogInputListener.COMPLETE, editTextDialogInput.getText().toString());
514                            }
515                        }).setNegativeButton("Cancel",
516                        new DialogInterface.OnClickListener() {
517
518                            public void onClick(DialogInterface dialog, int whichButton) {
519                                /* User clicked CANCEL, send CANCEL action
520                                 * and text */
521                                listener.onSoftText(SoftTextDialogInputListener.CANCEL, editTextDialogInput.getText().toString());
522                            }
523                        }).create();
524
525                dialogTextInput.show();
526            }
527        });
528    }
529}
530