package com.jme3.app; | |
import android.app.Activity; | |
import android.app.AlertDialog; | |
import android.content.DialogInterface; | |
import android.content.pm.ActivityInfo; | |
import android.graphics.drawable.Drawable; | |
import android.graphics.drawable.NinePatchDrawable; | |
import android.opengl.GLSurfaceView; | |
import android.os.Bundle; | |
import android.view.ViewGroup.LayoutParams; | |
import android.view.*; | |
import android.widget.FrameLayout; | |
import android.widget.ImageView; | |
import android.widget.TextView; | |
import com.jme3.audio.AudioRenderer; | |
import com.jme3.audio.android.AndroidAudioRenderer; | |
import com.jme3.input.android.AndroidInput; | |
import com.jme3.input.controls.TouchListener; | |
import com.jme3.input.event.TouchEvent; | |
import com.jme3.system.AppSettings; | |
import com.jme3.system.JmeSystem; | |
import com.jme3.system.android.AndroidConfigChooser.ConfigType; | |
import com.jme3.system.android.JmeAndroidSystem; | |
import com.jme3.system.android.OGLESContext; | |
import com.jme3.util.JmeFormatter; | |
import java.io.PrintWriter; | |
import java.io.StringWriter; | |
import java.util.logging.Handler; | |
import java.util.logging.Level; | |
import java.util.logging.Logger; | |
/** | |
* <code>AndroidHarness</code> wraps a jme application object and runs it on | |
* Android | |
* | |
* @author Kirill | |
* @author larynx | |
*/ | |
public class AndroidHarness extends Activity implements TouchListener, DialogInterface.OnClickListener { | |
protected final static Logger logger = Logger.getLogger(AndroidHarness.class.getName()); | |
/** | |
* The application class to start | |
*/ | |
protected String appClass = "jme3test.android.Test"; | |
/** | |
* The jme3 application object | |
*/ | |
protected Application app = null; | |
/** | |
* ConfigType.FASTEST is RGB565, GLSurfaceView default ConfigType.BEST is | |
* RGBA8888 or better if supported by the hardware | |
*/ | |
protected ConfigType eglConfigType = ConfigType.FASTEST; | |
/** | |
* If true all valid and not valid egl configs are logged | |
*/ | |
protected boolean eglConfigVerboseLogging = false; | |
/** | |
* If true MouseEvents are generated from TouchEvents | |
*/ | |
protected boolean mouseEventsEnabled = true; | |
/** | |
* Flip X axis | |
*/ | |
protected boolean mouseEventsInvertX = true; | |
/** | |
* Flip Y axis | |
*/ | |
protected boolean mouseEventsInvertY = true; | |
/** | |
* if true finish this activity when the jme app is stopped | |
*/ | |
protected boolean finishOnAppStop = 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; | |
/** | |
* Set the screen orientation, default is SENSOR | |
* ActivityInfo.SCREEN_ORIENTATION_* constants package | |
* android.content.pm.ActivityInfo | |
* | |
* SCREEN_ORIENTATION_UNSPECIFIED SCREEN_ORIENTATION_LANDSCAPE | |
* SCREEN_ORIENTATION_PORTRAIT SCREEN_ORIENTATION_USER | |
* SCREEN_ORIENTATION_BEHIND SCREEN_ORIENTATION_SENSOR (default) | |
* SCREEN_ORIENTATION_NOSENSOR | |
*/ | |
protected int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; | |
protected OGLESContext ctx; | |
protected GLSurfaceView view = null; | |
protected boolean isGLThreadPaused = true; | |
private ImageView splashImageView = null; | |
private FrameLayout frameLayout = null; | |
final private String ESCAPE_EVENT = "TouchEscape"; | |
static { | |
try { | |
System.loadLibrary("bulletjme"); | |
} catch (UnsatisfiedLinkError e) { | |
} | |
JmeSystem.setSystemDelegate(new JmeAndroidSystem()); | |
} | |
@Override | |
public void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
Logger log = logger; | |
boolean bIsLogFormatSet = false; | |
do { | |
if (log.getHandlers().length == 0) { | |
log = log.getParent(); | |
if (log != null) { | |
for (Handler h : log.getHandlers()) { | |
//h.setFormatter(new SimpleFormatter()); | |
h.setFormatter(new JmeFormatter()); | |
bIsLogFormatSet = true; | |
} | |
} | |
} | |
} while (log != null && !bIsLogFormatSet); | |
JmeAndroidSystem.setResources(getResources()); | |
JmeAndroidSystem.setActivity(this); | |
if (screenFullScreen) { | |
requestWindowFeature(Window.FEATURE_NO_TITLE); | |
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, | |
WindowManager.LayoutParams.FLAG_FULLSCREEN); | |
} else { | |
if (!screenShowTitle) { | |
requestWindowFeature(Window.FEATURE_NO_TITLE); | |
} | |
} | |
setRequestedOrientation(screenOrientation); | |
// Create Settings | |
AppSettings settings = new AppSettings(true); | |
// Create the input class | |
AndroidInput input = new AndroidInput(this); | |
input.setMouseEventsInvertX(mouseEventsInvertX); | |
input.setMouseEventsInvertY(mouseEventsInvertY); | |
input.setMouseEventsEnabled(mouseEventsEnabled); | |
// Create application instance | |
try { | |
if (app == null) { | |
@SuppressWarnings("unchecked") | |
Class<? extends Application> clazz = (Class<? extends Application>) Class.forName(appClass); | |
app = clazz.newInstance(); | |
} | |
app.setSettings(settings); | |
app.start(); | |
ctx = (OGLESContext) app.getContext(); | |
view = ctx.createView(input, eglConfigType, eglConfigVerboseLogging); | |
// Set the screen reolution | |
WindowManager wind = this.getWindowManager(); | |
Display disp = wind.getDefaultDisplay(); | |
ctx.getSettings().setResolution(disp.getWidth(), disp.getHeight()); | |
AppSettings s = ctx.getSettings(); | |
logger.log(Level.INFO, "Settings: Width {0} Height {1}", new Object[]{s.getWidth(), s.getHeight()}); | |
layoutDisplay(); | |
} catch (Exception ex) { | |
handleError("Class " + appClass + " init failed", ex); | |
setContentView(new TextView(this)); | |
} | |
} | |
@Override | |
protected void onRestart() { | |
super.onRestart(); | |
if (app != null) { | |
app.restart(); | |
} | |
logger.info("onRestart"); | |
} | |
@Override | |
protected void onStart() { | |
super.onStart(); | |
logger.info("onStart"); | |
} | |
@Override | |
protected void onResume() { | |
super.onResume(); | |
if (view != null) { | |
view.onResume(); | |
} | |
//resume the audio | |
AudioRenderer result = app.getAudioRenderer(); | |
if (result != null) { | |
if (result instanceof AndroidAudioRenderer) { | |
AndroidAudioRenderer renderer = (AndroidAudioRenderer) result; | |
renderer.resumeAll(); | |
} | |
} | |
isGLThreadPaused = false; | |
logger.info("onResume"); | |
} | |
@Override | |
protected void onPause() { | |
super.onPause(); | |
if (view != null) { | |
view.onPause(); | |
} | |
//pause the audio | |
AudioRenderer result = app.getAudioRenderer(); | |
if (result != null) { | |
logger.info("pause: " + result.getClass().getSimpleName()); | |
if (result instanceof AndroidAudioRenderer) { | |
AndroidAudioRenderer renderer = (AndroidAudioRenderer) result; | |
renderer.pauseAll(); | |
} | |
} | |
isGLThreadPaused = true; | |
logger.info("onPause"); | |
} | |
@Override | |
protected void onStop() { | |
super.onStop(); | |
logger.info("onStop"); | |
} | |
@Override | |
protected void onDestroy() { | |
if (app != null) { | |
app.stop(!isGLThreadPaused); | |
} | |
logger.info("onDestroy"); | |
super.onDestroy(); | |
} | |
public Application getJmeApplication() { | |
return app; | |
} | |
/** | |
* Called when an error has occurred. By default, will show an error message | |
* to the user and print the exception/error to the log. | |
*/ | |
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); | |
runOnUiThread(new Runnable() { | |
@Override | |
public void run() { | |
AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon) | |
.setTitle(finalTitle).setPositiveButton("Kill", AndroidHarness.this).setMessage(finalMsg).create(); | |
dialog.show(); | |
} | |
}); | |
} | |
/** | |
* Called by the android alert dialog, terminate the activity and OpenGL | |
* rendering | |
* | |
* @param dialog | |
* @param whichButton | |
*/ | |
public void onClick(DialogInterface dialog, int whichButton) { | |
if (whichButton != -2) { | |
if (app != null) { | |
app.stop(true); | |
} | |
this.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: | |
runOnUiThread(new Runnable() { | |
@Override | |
public void run() { | |
AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon) | |
.setTitle(exitDialogTitle).setPositiveButton("Yes", AndroidHarness.this).setNegativeButton("No", AndroidHarness.this).setMessage(exitDialogMessage).create(); | |
dialog.show(); | |
} | |
}); | |
break; | |
default: | |
break; | |
} | |
} | |
} | |
public void layoutDisplay() { | |
logger.log(Level.INFO, "Splash Screen Picture Resource ID: {0}", splashPicID); | |
if (splashPicID != 0) { | |
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( | |
LayoutParams.FILL_PARENT, | |
LayoutParams.FILL_PARENT, | |
Gravity.CENTER); | |
frameLayout = new FrameLayout(this); | |
splashImageView = new ImageView(this); | |
Drawable drawable = this.getResources().getDrawable(splashPicID); | |
if (drawable instanceof NinePatchDrawable) { | |
splashImageView.setBackgroundDrawable(drawable); | |
} else { | |
splashImageView.setImageResource(splashPicID); | |
} | |
frameLayout.addView(view); | |
frameLayout.addView(splashImageView, lp); | |
setContentView(frameLayout); | |
logger.log(Level.INFO, "Splash Screen Created"); | |
} else { | |
logger.log(Level.INFO, "Splash Screen Skipped."); | |
setContentView(view); | |
} | |
} | |
public void removeSplashScreen() { | |
logger.log(Level.INFO, "Splash Screen Picture Resource ID: {0}", splashPicID); | |
if (splashPicID != 0) { | |
if (frameLayout != null) { | |
if (splashImageView != null) { | |
this.runOnUiThread(new Runnable() { | |
@Override | |
public void run() { | |
splashImageView.setVisibility(View.INVISIBLE); | |
frameLayout.removeView(splashImageView); | |
} | |
}); | |
} else { | |
logger.log(Level.INFO, "splashImageView is null"); | |
} | |
} else { | |
logger.log(Level.INFO, "frameLayout is null"); | |
} | |
} | |
} | |
public boolean isFinishOnAppStop() { | |
return finishOnAppStop; | |
} | |
} |