1 package com.jme3.app; 2 3 import android.app.Activity; 4 import android.app.AlertDialog; 5 import android.content.DialogInterface; 6 import android.content.pm.ActivityInfo; 7 import android.graphics.drawable.Drawable; 8 import android.graphics.drawable.NinePatchDrawable; 9 import android.opengl.GLSurfaceView; 10 import android.os.Bundle; 11 import android.view.ViewGroup.LayoutParams; 12 import android.view.*; 13 import android.widget.FrameLayout; 14 import android.widget.ImageView; 15 import android.widget.TextView; 16 import com.jme3.audio.AudioRenderer; 17 import com.jme3.audio.android.AndroidAudioRenderer; 18 import com.jme3.input.android.AndroidInput; 19 import com.jme3.input.controls.TouchListener; 20 import com.jme3.input.event.TouchEvent; 21 import com.jme3.system.AppSettings; 22 import com.jme3.system.JmeSystem; 23 import com.jme3.system.android.AndroidConfigChooser.ConfigType; 24 import com.jme3.system.android.JmeAndroidSystem; 25 import com.jme3.system.android.OGLESContext; 26 import com.jme3.util.JmeFormatter; 27 import java.io.PrintWriter; 28 import java.io.StringWriter; 29 import java.util.logging.Handler; 30 import java.util.logging.Level; 31 import java.util.logging.Logger; 32 33 /** 34 * <code>AndroidHarness</code> wraps a jme application object and runs it on 35 * Android 36 * 37 * @author Kirill 38 * @author larynx 39 */ 40 public class AndroidHarness extends Activity implements TouchListener, DialogInterface.OnClickListener { 41 42 protected final static Logger logger = Logger.getLogger(AndroidHarness.class.getName()); 43 /** 44 * The application class to start 45 */ 46 protected String appClass = "jme3test.android.Test"; 47 /** 48 * The jme3 application object 49 */ 50 protected Application app = null; 51 /** 52 * ConfigType.FASTEST is RGB565, GLSurfaceView default ConfigType.BEST is 53 * RGBA8888 or better if supported by the hardware 54 */ 55 protected ConfigType eglConfigType = ConfigType.FASTEST; 56 /** 57 * If true all valid and not valid egl configs are logged 58 */ 59 protected boolean eglConfigVerboseLogging = false; 60 /** 61 * If true MouseEvents are generated from TouchEvents 62 */ 63 protected boolean mouseEventsEnabled = true; 64 /** 65 * Flip X axis 66 */ 67 protected boolean mouseEventsInvertX = true; 68 /** 69 * Flip Y axis 70 */ 71 protected boolean mouseEventsInvertY = true; 72 /** 73 * if true finish this activity when the jme app is stopped 74 */ 75 protected boolean finishOnAppStop = true; 76 /** 77 * Title of the exit dialog, default is "Do you want to exit?" 78 */ 79 protected String exitDialogTitle = "Do you want to exit?"; 80 /** 81 * Message of the exit dialog, default is "Use your home key to bring this 82 * app into the background or exit to terminate it." 83 */ 84 protected String exitDialogMessage = "Use your home key to bring this app into the background or exit to terminate it."; 85 /** 86 * Set the screen window mode. If screenFullSize is true, then the 87 * notification bar and title bar are removed and the screen covers the 88 * entire display. If screenFullSize is false, then the notification bar 89 * remains visible if screenShowTitle is true while screenFullScreen is 90 * false, then the title bar is also displayed under the notification bar. 91 */ 92 protected boolean screenFullScreen = true; 93 /** 94 * if screenShowTitle is true while screenFullScreen is false, then the 95 * title bar is also displayed under the notification bar 96 */ 97 protected boolean screenShowTitle = true; 98 /** 99 * Splash Screen picture Resource ID. If a Splash Screen is desired, set 100 * splashPicID to the value of the Resource ID (i.e. R.drawable.picname). If 101 * splashPicID = 0, then no splash screen will be displayed. 102 */ 103 protected int splashPicID = 0; 104 /** 105 * Set the screen orientation, default is SENSOR 106 * ActivityInfo.SCREEN_ORIENTATION_* constants package 107 * android.content.pm.ActivityInfo 108 * 109 * SCREEN_ORIENTATION_UNSPECIFIED SCREEN_ORIENTATION_LANDSCAPE 110 * SCREEN_ORIENTATION_PORTRAIT SCREEN_ORIENTATION_USER 111 * SCREEN_ORIENTATION_BEHIND SCREEN_ORIENTATION_SENSOR (default) 112 * SCREEN_ORIENTATION_NOSENSOR 113 */ 114 protected int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; 115 protected OGLESContext ctx; 116 protected GLSurfaceView view = null; 117 protected boolean isGLThreadPaused = true; 118 private ImageView splashImageView = null; 119 private FrameLayout frameLayout = null; 120 final private String ESCAPE_EVENT = "TouchEscape"; 121 122 static { 123 try { 124 System.loadLibrary("bulletjme"); 125 } catch (UnsatisfiedLinkError e) { 126 } 127 JmeSystem.setSystemDelegate(new JmeAndroidSystem()); 128 } 129 130 @Override 131 public void onCreate(Bundle savedInstanceState) { 132 super.onCreate(savedInstanceState); 133 134 Logger log = logger; 135 boolean bIsLogFormatSet = false; 136 do { 137 if (log.getHandlers().length == 0) { 138 log = log.getParent(); 139 if (log != null) { 140 for (Handler h : log.getHandlers()) { 141 //h.setFormatter(new SimpleFormatter()); 142 h.setFormatter(new JmeFormatter()); 143 bIsLogFormatSet = true; 144 } 145 } 146 } 147 } while (log != null && !bIsLogFormatSet); 148 149 JmeAndroidSystem.setResources(getResources()); 150 JmeAndroidSystem.setActivity(this); 151 152 if (screenFullScreen) { 153 requestWindowFeature(Window.FEATURE_NO_TITLE); 154 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 155 WindowManager.LayoutParams.FLAG_FULLSCREEN); 156 } else { 157 if (!screenShowTitle) { 158 requestWindowFeature(Window.FEATURE_NO_TITLE); 159 } 160 } 161 162 setRequestedOrientation(screenOrientation); 163 164 // Create Settings 165 AppSettings settings = new AppSettings(true); 166 167 // Create the input class 168 AndroidInput input = new AndroidInput(this); 169 input.setMouseEventsInvertX(mouseEventsInvertX); 170 input.setMouseEventsInvertY(mouseEventsInvertY); 171 input.setMouseEventsEnabled(mouseEventsEnabled); 172 173 // Create application instance 174 try { 175 if (app == null) { 176 @SuppressWarnings("unchecked") 177 Class<? extends Application> clazz = (Class<? extends Application>) Class.forName(appClass); 178 app = clazz.newInstance(); 179 } 180 181 app.setSettings(settings); 182 app.start(); 183 ctx = (OGLESContext) app.getContext(); 184 view = ctx.createView(input, eglConfigType, eglConfigVerboseLogging); 185 186 // Set the screen reolution 187 WindowManager wind = this.getWindowManager(); 188 Display disp = wind.getDefaultDisplay(); 189 ctx.getSettings().setResolution(disp.getWidth(), disp.getHeight()); 190 191 AppSettings s = ctx.getSettings(); 192 logger.log(Level.INFO, "Settings: Width {0} Height {1}", new Object[]{s.getWidth(), s.getHeight()}); 193 194 layoutDisplay(); 195 } catch (Exception ex) { 196 handleError("Class " + appClass + " init failed", ex); 197 setContentView(new TextView(this)); 198 } 199 } 200 201 @Override 202 protected void onRestart() { 203 super.onRestart(); 204 if (app != null) { 205 app.restart(); 206 } 207 208 logger.info("onRestart"); 209 } 210 211 @Override 212 protected void onStart() { 213 super.onStart(); 214 logger.info("onStart"); 215 } 216 217 @Override 218 protected void onResume() { 219 super.onResume(); 220 if (view != null) { 221 view.onResume(); 222 } 223 224 //resume the audio 225 AudioRenderer result = app.getAudioRenderer(); 226 if (result != null) { 227 if (result instanceof AndroidAudioRenderer) { 228 AndroidAudioRenderer renderer = (AndroidAudioRenderer) result; 229 renderer.resumeAll(); 230 } 231 } 232 233 isGLThreadPaused = false; 234 logger.info("onResume"); 235 } 236 237 @Override 238 protected void onPause() { 239 super.onPause(); 240 if (view != null) { 241 view.onPause(); 242 } 243 244 //pause the audio 245 AudioRenderer result = app.getAudioRenderer(); 246 if (result != null) { 247 logger.info("pause: " + result.getClass().getSimpleName()); 248 if (result instanceof AndroidAudioRenderer) { 249 AndroidAudioRenderer renderer = (AndroidAudioRenderer) result; 250 renderer.pauseAll(); 251 } 252 } 253 254 isGLThreadPaused = true; 255 logger.info("onPause"); 256 } 257 258 @Override 259 protected void onStop() { 260 super.onStop(); 261 262 logger.info("onStop"); 263 } 264 265 @Override 266 protected void onDestroy() { 267 if (app != null) { 268 app.stop(!isGLThreadPaused); 269 } 270 271 logger.info("onDestroy"); 272 super.onDestroy(); 273 } 274 275 public Application getJmeApplication() { 276 return app; 277 } 278 279 /** 280 * Called when an error has occurred. By default, will show an error message 281 * to the user and print the exception/error to the log. 282 */ 283 public void handleError(final String errorMsg, final Throwable t) { 284 String stackTrace = ""; 285 String title = "Error"; 286 287 if (t != null) { 288 // Convert exception to string 289 StringWriter sw = new StringWriter(100); 290 t.printStackTrace(new PrintWriter(sw)); 291 stackTrace = sw.toString(); 292 title = t.toString(); 293 } 294 295 final String finalTitle = title; 296 final String finalMsg = (errorMsg != null ? errorMsg : "Uncaught Exception") 297 + "\n" + stackTrace; 298 299 logger.log(Level.SEVERE, finalMsg); 300 301 runOnUiThread(new Runnable() { 302 303 @Override 304 public void run() { 305 AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon) 306 .setTitle(finalTitle).setPositiveButton("Kill", AndroidHarness.this).setMessage(finalMsg).create(); 307 dialog.show(); 308 } 309 }); 310 } 311 312 /** 313 * Called by the android alert dialog, terminate the activity and OpenGL 314 * rendering 315 * 316 * @param dialog 317 * @param whichButton 318 */ 319 public void onClick(DialogInterface dialog, int whichButton) { 320 if (whichButton != -2) { 321 if (app != null) { 322 app.stop(true); 323 } 324 this.finish(); 325 } 326 } 327 328 /** 329 * Gets called by the InputManager on all touch/drag/scale events 330 */ 331 @Override 332 public void onTouch(String name, TouchEvent evt, float tpf) { 333 if (name.equals(ESCAPE_EVENT)) { 334 switch (evt.getType()) { 335 case KEY_UP: 336 runOnUiThread(new Runnable() { 337 338 @Override 339 public void run() { 340 AlertDialog dialog = new AlertDialog.Builder(AndroidHarness.this) // .setIcon(R.drawable.alert_dialog_icon) 341 .setTitle(exitDialogTitle).setPositiveButton("Yes", AndroidHarness.this).setNegativeButton("No", AndroidHarness.this).setMessage(exitDialogMessage).create(); 342 dialog.show(); 343 } 344 }); 345 break; 346 default: 347 break; 348 } 349 } 350 } 351 352 public void layoutDisplay() { 353 logger.log(Level.INFO, "Splash Screen Picture Resource ID: {0}", splashPicID); 354 if (splashPicID != 0) { 355 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( 356 LayoutParams.FILL_PARENT, 357 LayoutParams.FILL_PARENT, 358 Gravity.CENTER); 359 360 frameLayout = new FrameLayout(this); 361 splashImageView = new ImageView(this); 362 363 Drawable drawable = this.getResources().getDrawable(splashPicID); 364 if (drawable instanceof NinePatchDrawable) { 365 splashImageView.setBackgroundDrawable(drawable); 366 } else { 367 splashImageView.setImageResource(splashPicID); 368 } 369 370 frameLayout.addView(view); 371 frameLayout.addView(splashImageView, lp); 372 373 setContentView(frameLayout); 374 logger.log(Level.INFO, "Splash Screen Created"); 375 } else { 376 logger.log(Level.INFO, "Splash Screen Skipped."); 377 setContentView(view); 378 } 379 } 380 381 public void removeSplashScreen() { 382 logger.log(Level.INFO, "Splash Screen Picture Resource ID: {0}", splashPicID); 383 if (splashPicID != 0) { 384 if (frameLayout != null) { 385 if (splashImageView != null) { 386 this.runOnUiThread(new Runnable() { 387 388 @Override 389 public void run() { 390 splashImageView.setVisibility(View.INVISIBLE); 391 frameLayout.removeView(splashImageView); 392 } 393 }); 394 } else { 395 logger.log(Level.INFO, "splashImageView is null"); 396 } 397 } else { 398 logger.log(Level.INFO, "frameLayout is null"); 399 } 400 } 401 } 402 403 public boolean isFinishOnAppStop() { 404 return finishOnAppStop; 405 } 406 407 408 } 409