From 6ec86fe919be780442572dd71730b2f516404bb2 Mon Sep 17 00:00:00 2001 From: iwgeric Date: Mon, 26 Jan 2015 21:28:11 -0500 Subject: [PATCH] Initial commit for support of launching a jME application as an Android Fragment --- .../com/jme3/app/AndroidHarnessFragment.java | 647 ++++++++++++++++++ 1 file changed, 647 insertions(+) create mode 100644 jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java diff --git a/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java new file mode 100644 index 000000000..c80d883d4 --- /dev/null +++ b/jme3-android/src/main/java/com/jme3/app/AndroidHarnessFragment.java @@ -0,0 +1,647 @@ +/* + * Copyright (c) 2009-2015 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.DialogInterface; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.NinePatchDrawable; +import android.opengl.GLSurfaceView; +import android.os.Bundle; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import com.jme3.audio.AudioRenderer; +import com.jme3.input.JoyInput; +import com.jme3.input.TouchInput; +import com.jme3.input.android.AndroidSensorJoyInput; +import com.jme3.input.controls.TouchListener; +import com.jme3.input.controls.TouchTrigger; +import com.jme3.input.event.TouchEvent; +import static com.jme3.input.event.TouchEvent.Type.KEY_UP; +import com.jme3.system.AppSettings; +import com.jme3.system.SystemListener; +import com.jme3.system.android.JmeAndroidSystem; +import com.jme3.system.android.OGLESContext; +import com.jme3.util.AndroidLogHandler; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * + * @author iwgeric + */ +public class AndroidHarnessFragment extends Fragment implements TouchListener, DialogInterface.OnClickListener, SystemListener { + private static final Logger logger = Logger.getLogger(AndroidHarnessFragment.class.getName()); + + /** + * The application class to start + */ + protected String appClass = "jme3test.android.Test"; + + /** + * Sets the desired RGB size for the surfaceview. 16 = RGB565, 24 = RGB888. + * (default = 24) + */ + protected int eglBitsPerPixel = 24; + + /** + * Sets the desired number of Alpha bits for the surfaceview. This affects + * how the surfaceview is able to display Android views that are located + * under the surfaceview jME uses to render the scenegraph. + * 0 = Opaque surfaceview background (fastest) + * 1->7 = Transparent surfaceview background + * 8 or higher = Translucent surfaceview background + * (default = 0) + */ + protected int eglAlphaBits = 0; + + /** + * The number of depth bits specifies the precision of the depth buffer. + * (default = 16) + */ + protected int eglDepthBits = 16; + + /** + * Sets the number of samples to use for multisampling.
+ * Leave 0 (default) to disable multisampling.
+ * Set to 2 or 4 to enable multisampling. + */ + protected int eglSamples = 0; + + /** + * Set the number of stencil bits. + * (default = 0) + */ + protected int eglStencilBits = 0; + + /** + * Set the desired frame rate. If frameRate > 0, the application + * will be capped at the desired frame rate. + * (default = -1, no frame rate cap) + */ + protected int frameRate = -1; + + /** + * Sets the type of Audio Renderer to be used. + *

+ * Android MediaPlayer / SoundPool can be used on all + * supported Android platform versions (2.2+)
+ * OpenAL Soft uses an OpenSL backend and is only supported on Android + * versions 2.3+. + *

+ * Only use ANDROID_ static strings found in AppSettings + * + */ + protected String audioRendererType = AppSettings.ANDROID_OPENAL_SOFT; + + /** + * If true Android Sensors are used as simulated Joysticks. Users can use the + * Android sensor feedback through the RawInputListener or by registering + * JoyAxisTriggers. + */ + protected boolean joystickEventsEnabled = false; + + /** + * If true KeyEvents are generated from TouchEvents + */ + protected boolean keyEventsEnabled = true; + + /** + * If true MouseEvents are generated from TouchEvents + */ + protected boolean mouseEventsEnabled = true; + + /** + * Flip X axis + */ + protected boolean mouseEventsInvertX = false; + + /** + * Flip Y axis + */ + protected boolean mouseEventsInvertY = false; + + /** + * if true finish this activity when the jme app is stopped + */ + protected boolean finishOnAppStop = true; + + /** + * set to false if you don't want the harness to handle the exit hook + */ + protected boolean handleExitHook = true; + + /** + * Title of the exit dialog, default is "Do you want to exit?" + */ + protected String exitDialogTitle = "Do you want to exit?"; + + /** + * Message of the exit dialog, default is "Use your home key to bring this + * app into the background or exit to terminate it." + */ + protected String exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it."; + + /** + * Set the screen window mode. If screenFullSize is true, then the + * notification bar and title bar are removed and the screen covers the + * entire display. If screenFullSize is false, then the notification bar + * remains visible if screenShowTitle is true while screenFullScreen is + * false, then the title bar is also displayed under the notification bar. + */ + protected boolean screenFullScreen = true; + /** + * if screenShowTitle is true while screenFullScreen is false, then the + * title bar is also displayed under the notification bar + */ + protected boolean screenShowTitle = true; + /** + * Splash Screen picture Resource ID. If a Splash Screen is desired, set + * splashPicID to the value of the Resource ID (i.e. R.drawable.picname). If + * splashPicID = 0, then no splash screen will be displayed. + */ + protected int splashPicID = 0; + + protected FrameLayout frameLayout = null; + protected GLSurfaceView view = null; + protected ImageView splashImageView = null; + final private String ESCAPE_EVENT = "TouchEscape"; + private boolean firstDrawFrame = true; + private Application app = null; + + // Retrieves the jME application object + public Application getJmeApplication() { + return app; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + } + + /** + * This Fragment uses setRetainInstance(true) so the onCreate method will only + * be called once. During device configuration changes, the instance of + * this Fragment will be reused in the new Activity. This method should not + * contain any View related objects. They are created and destroyed by + * other methods. View related objects should not be reused, but rather + * created and destroyed along with the Activity. + * + * @param savedInstanceState + */ + @Override + public void onCreate(Bundle savedInstanceState) { + initializeLogHandler(); + logger.fine("onCreate"); + super.onCreate(savedInstanceState); + + // Create Settings + logger.log(Level.FINE, "Creating settings"); + AppSettings settings = new AppSettings(true); + settings.setEmulateMouse(mouseEventsEnabled); + settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY); + settings.setUseJoysticks(joystickEventsEnabled); + settings.setEmulateKeyboard(keyEventsEnabled); + + settings.setBitsPerPixel(eglBitsPerPixel); + settings.setAlphaBits(eglAlphaBits); + settings.setDepthBits(eglDepthBits); + settings.setSamples(eglSamples); + settings.setStencilBits(eglStencilBits); + settings.setAudioRenderer(audioRendererType); + + settings.setFrameRate(frameRate); + + // Create application instance + try { + if (app == null) { + @SuppressWarnings("unchecked") + Class clazz = (Class) Class.forName(appClass); + app = clazz.newInstance(); + } + + app.setSettings(settings); + app.start(); + } catch (Exception ex) { + handleError("Class " + appClass + " init failed", ex); + } + + OGLESContext ctx = (OGLESContext) app.getContext(); + // AndroidHarness wraps the app as a SystemListener. + ctx.setSystemListener(this); + + setRetainInstance(true); + } + + /** + * Called by the system to create the View hierchy associated with this + * Fragment. For jME, this is a FrameLayout that contains the GLSurfaceView + * and an overlaying SplashScreen Image (if used). The View that is returned + * will be placed on the screen within the boundries of the View borders defined + * by the Activity's layout parameters for this Fragment. For jME, we also + * update the application reference to the new view. + * + * @param inflater + * @param container + * @param savedInstanceState + * @return + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + logger.fine("onCreateView"); + // Create the GLSurfaceView for the application + view = ((OGLESContext) app.getContext()).createView(getActivity()); + // store the glSurfaceView in JmeAndroidSystem for future use + JmeAndroidSystem.setView(view); + createLayout(); + return frameLayout; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + logger.fine("onActivityCreated"); + super.onActivityCreated(savedInstanceState); + } + + @Override + public void onStart() { + logger.fine("onStart"); + super.onStart(); + } + + /** + * When the Fragment resumes (ie. after app resumes or device screen turned + * back on), call the gainFocus() in the jME application. + */ + @Override + public void onResume() { + logger.fine("onResume"); + super.onResume(); + + gainFocus(); + } + + /** + * When the Fragment pauses (ie. after home button pressed on the device + * or device screen turned off) , call the loseFocus() in the jME application. + */ + @Override + public void onPause() { + logger.fine("onPause"); + loseFocus(); + + super.onPause(); + } + + @Override + public void onStop() { + logger.fine("onStop"); + super.onStop(); + } + + /** + * Called by the Android system each time the Activity is destroyed or recreated. + * For jME, we clear references to the GLSurfaceView. + */ + @Override + public void onDestroyView() { + logger.fine("onDestroyView"); + view = null; + JmeAndroidSystem.setView(null); + + super.onDestroyView(); + } + + /** + * Called by the system when the application is being destroyed. In this case, + * the jME application is actually closed as well. This method is not called + * during device configuration changes or when the application is put in the + * background. + */ + @Override + public void onDestroy() { + logger.fine("onDestroy"); + if (app != null) { + app.stop(false); + } + app = null; + super.onDestroy(); + } + + @Override + public void onDetach() { + logger.fine("onDetach"); + super.onDetach(); + } + + + /** + * Called when an error has occurred. By default, will show an error message + * to the user and print the exception/error to the log. + */ + @Override + public void handleError(final String errorMsg, final Throwable t) { + String stackTrace = ""; + String title = "Error"; + + if (t != null) { + // Convert exception to string + StringWriter sw = new StringWriter(100); + t.printStackTrace(new PrintWriter(sw)); + stackTrace = sw.toString(); + title = t.toString(); + } + + final String finalTitle = title; + final String finalMsg = (errorMsg != null ? errorMsg : "Uncaught Exception") + + "\n" + stackTrace; + + logger.log(Level.SEVERE, finalMsg); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(finalTitle); + builder.setPositiveButton("Kill", AndroidHarnessFragment.this); + builder.setMessage(finalMsg); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + }); + } + + /** + * Called by the android alert dialog, terminate the activity and OpenGL + * rendering + * + * @param dialog + * @param whichButton + */ + @Override + public void onClick(DialogInterface dialog, int whichButton) { + if (whichButton != -2) { + if (app != null) { + app.stop(true); + } + app = null; + getActivity().finish(); + } + } + + /** + * Gets called by the InputManager on all touch/drag/scale events + */ + @Override + public void onTouch(String name, TouchEvent evt, float tpf) { + if (name.equals(ESCAPE_EVENT)) { + switch (evt.getType()) { + case KEY_UP: + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(exitDialogTitle); + builder.setPositiveButton("Yes", AndroidHarnessFragment.this); + builder.setNegativeButton("No", AndroidHarnessFragment.this); + builder.setMessage(exitDialogMessage); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + }); + break; + default: + break; + } + } + } + + public void createLayout() { + logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + Gravity.CENTER); + + if (frameLayout != null && frameLayout.getParent() != null) { + ((ViewGroup) frameLayout.getParent()).removeView(frameLayout); + } + frameLayout = new FrameLayout(getActivity()); + + if (view.getParent() != null) { + ((ViewGroup) view.getParent()).removeView(view); + } + frameLayout.addView(view); + + if (splashPicID != 0) { + splashImageView = new ImageView(getActivity()); + + Drawable drawable = getResources().getDrawable(splashPicID); + if (drawable instanceof NinePatchDrawable) { + splashImageView.setBackgroundDrawable(drawable); + } else { + splashImageView.setImageResource(splashPicID); + } + + if (splashImageView.getParent() != null) { + ((ViewGroup) splashImageView.getParent()).removeView(splashImageView); + } + frameLayout.addView(splashImageView, lp); + + logger.log(Level.FINE, "Splash Screen Created"); + } else { + logger.log(Level.FINE, "Splash Screen Skipped."); + } + } + + public void removeSplashScreen() { + logger.log(Level.FINE, "Splash Screen Picture Resource ID: {0}", splashPicID); + if (splashPicID != 0) { + if (frameLayout != null) { + if (splashImageView != null) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + splashImageView.setVisibility(View.INVISIBLE); + frameLayout.removeView(splashImageView); + } + }); + } else { + logger.log(Level.FINE, "splashImageView is null"); + } + } else { + logger.log(Level.FINE, "frameLayout is null"); + } + } + } + + /** + * Removes the standard Android log handler due to an issue with not logging + * entries lower than INFO level and adds a handler that produces + * JME formatted log messages. + */ + protected void initializeLogHandler() { + Logger log = LogManager.getLogManager().getLogger(""); + for (Handler handler : log.getHandlers()) { + if (log.getLevel() != null && log.getLevel().intValue() <= Level.FINE.intValue()) { + Log.v("AndroidHarness", "Removing Handler class: " + handler.getClass().getName()); + } + log.removeHandler(handler); + } + Handler handler = new AndroidLogHandler(); + log.addHandler(handler); + handler.setLevel(Level.ALL); + } + + @Override + public void initialize() { + app.initialize(); + if (handleExitHook) { + // remove existing mapping from SimpleApplication that stops the app + // when the esc key is pressed (esc key = android back key) so that + // AndroidHarness can produce the exit app dialog box. + if (app.getInputManager().hasMapping(SimpleApplication.INPUT_MAPPING_EXIT)) { + app.getInputManager().deleteMapping(SimpleApplication.INPUT_MAPPING_EXIT); + } + + app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK)); + app.getInputManager().addListener(this, new String[]{ESCAPE_EVENT}); + } + } + + @Override + public void reshape(int width, int height) { + app.reshape(width, height); + } + + @Override + public void update() { + app.update(); + // call to remove the splash screen, if present. + // call after app.update() to make sure no gap between + // splash screen going away and app display being shown. + if (firstDrawFrame) { + removeSplashScreen(); + firstDrawFrame = false; + } + } + + @Override + public void requestClose(boolean esc) { + app.requestClose(esc); + } + + @Override + public void destroy() { + if (app != null) { + app.destroy(); + } + if (finishOnAppStop) { + getActivity().finish(); + } + } + + @Override + public void gainFocus() { + logger.fine("gainFocus"); + if (view != null) { + view.onResume(); + } + + if (app != null) { + //resume the audio + AudioRenderer audioRenderer = app.getAudioRenderer(); + if (audioRenderer != null) { + audioRenderer.resumeAll(); + } + //resume the sensors (aka joysticks) + if (app.getContext() != null) { + JoyInput joyInput = app.getContext().getJoyInput(); + if (joyInput != null) { + if (joyInput instanceof AndroidSensorJoyInput) { + AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; + androidJoyInput.resumeSensors(); + } + } + } + } + + if (app != null) { + app.gainFocus(); + } + } + + @Override + public void loseFocus() { + logger.fine("loseFocus"); + if (app != null) { + app.loseFocus(); + } + + if (view != null) { + view.onPause(); + } + + if (app != null) { + //pause the audio + AudioRenderer audioRenderer = app.getAudioRenderer(); + if (audioRenderer != null) { + audioRenderer.pauseAll(); + } + //pause the sensors (aka joysticks) + if (app.getContext() != null) { + JoyInput joyInput = app.getContext().getJoyInput(); + if (joyInput != null) { + if (joyInput instanceof AndroidSensorJoyInput) { + AndroidSensorJoyInput androidJoyInput = (AndroidSensorJoyInput) joyInput; + androidJoyInput.pauseSensors(); + } + } + } + } + + } + +}