Supporting Device Configuration Changes

I’ve started to look into how jme could support not restarting the game when the device configuration changes. If anyone has already worked on that, please let me know.



The main problem I see today is that Android will destroy the activity and restart it when a device configuration is detected which in turn destroys the game and recreates it. While it is true that even today the app can save and restore its own game states, I thought the engine should be able to handle simple screen orientation changes without making the user deal with it themselves and creating significant delay while the game loads again.



The previous idea I found in some posts dealt with adding android:configChanges to the mainfest and then using onConfigurationChanged to resize the app. This approach seemed to have some limitations (and is now stated by google as only being appropriate as a last result).



In some special cases, you may want to bypass restarting of your activity based on one or more types of configuration changes. This is done with the android:configChanges attribute in its manifest. For any types of configuration changes you say that you handle there, you will receive a call to your current activity’s onConfigurationChanged(Configuration) method instead of being restarted. If a configuration change involves any that you do not handle, however, the activity will still be restarted and onConfigurationChanged(Configuration) will not be called.



The idea I started investigating was to use the onRetainNonConfigurationInstance() method of the activity to save the app and view and then restore it in onCreate() in AndroidHarness.



http://developer.android.com/guide/topics/resources/runtime-changes.html



Unfortunately, this method is now depreciated and has been replaced with Fragments starting with API 11 (I think), so I need to figure out what the right approach should be. While most of the Fragments design goals don’t matter much for jme on Android since it really only interacts with a single surfaceview, it includes the idea of maintaining a fragment while the main activity is destroyed and recreated on config changes. There is even a static library available to make Fragments available on older platforms even before API 8 which is jme’s current targeted starting version.



Below are some links that I plan to read and understand, but if anyone has any concerns and/or other ideas, I would appreciate hearing them.



http://developer.android.com/reference/android/app/Fragment.html



http://android-developers.blogspot.com/2011/03/fragments-for-all.html



http://developer.android.com/guide/components/fragments.html



[EDIT: adding another link] http://developer.android.com/reference/android/app/Fragment.html#setRetainInstance(boolean)



I’m going to start digging in unless someone thinks this is a bad idea in general.

Why can’t we just use onConfigurationChanged(Configuration) and then change the AppSettings of the jME3 Application object? We already support resizing and resolution changes on desktop platforms, I don’t see why we need to keep the GLSurfaceView the same.

The resolution changes should already be handled by the surfaceview call onSurfaceChanged which is in either OGLESContext or AndroidInput (can’t remember which right now).



Check out the “Handling the Configuration Change Yourself” section in this link. http://developer.android.com/guide/topics/resources/runtime-changes.html#HandlingTheChange



It says:

If your application doesn’t need to update resources during a specific configuration change and you have a performance limitation that requires you to avoid the activity restart, then you can declare that your activity handles the configuration change itself, which prevents the system from restarting your activity.



Note: Handling the configuration change yourself can make it much more difficult to use alternative resources, because the system does not automatically apply them for you. This technique should be considered a last resort when you must avoid restarts due to a configuration change and is not recommended for most applications.




I believe @nehon also put some work into AndroidHarness to allow users to have the surfaceview only be a part of the screen so that other Android specific views could also be included on the screen like AdMob advertisements. If jme only supported the onConfigurationChange method, the non-jme resources would not be updated automatically as there normally are.



I’m not sure what I’m proposing is really the right way or not, but I thought it is worth a look. If jme can support this and also allow users to have non-jme views also on the screen, then I thought it would be a better solution.



If jme is going to be limited to only have the surfaceview on the screen, then onConfigurationChanged is probably good enough. I’ll play around with it to see if there are other issues with using that method.

@iwgeric said:
The resolution changes should already be handled by the surfaceview call onSurfaceChanged which is in either OGLESContext or AndroidInput (can't remember which right now).

I am not talking about changing resolutions. That was simply an example since on desktop, resolution changes require restarting the application (or the context for that matter). What I mean is that we can apply the same concept to handle Android configuration changes.

What happens when you set the "android:configChanges" flag but make no changes in jME3? Does the screen have the wrong orientation / resolution? Does it crash?

It’s been awhile since I tried it, but I think jme will react fine when the “android:configChanges” flag is used in the manifest. The issue with using this method is that the non-jme items on the screen will not be updated based on the configuration change. These other items are typically laid out in seperate xml files for each screen configuration so that the system can load the correct one based on the current device configuration (ie. place ads on the right of the surfaceview when in landscape, but on top if in portrait mode).



I’ll set up a new project using this flag so I can confirm exactly what happens.

@iwgeric said:
It's been awhile since I tried it, but I think jme will react fine when the “android:configChanges” flag is used in the manifest. The issue with using this method is that the non-jme items on the screen will not be updated based on the configuration change. These other items are typically laid out in seperate xml files for each screen configuration so that the system can load the correct one based on the current device configuration (ie. place ads on the right of the surfaceview when in landscape, but on top if in portrait mode).

I'll set up a new project using this flag so I can confirm exactly what happens.

So that means the GLSurfaceView will be destroyed then? I assume so since the content view gets replaced with the new XML file.
In that case, how are you supposed to inject it into the new XML? Or is there another way of doing this?

Yes, normally when a device changes configuration, the entire activity is destroyed and recreated. This includes the GLSurfaceView. So far, I’ve only had my apps forced to landscape mode all the time. In this mode, the activity never changes orientation, so the app stays alive all the time. However, I’d like to change it to Sensor mode so that it will change orientation with the device. I’m also considering placing admob advertisements on my app. These are Android specific and can’t be included inside the jme3 app, so I need to let Android deal with the orientation change. The problem with onConfigurationChange is that the activity is not destroyed and recreated, so the layout of any non-jme entities will have to be handled via code instead of the normal XML definition way.

@iwgeric said:
Yes, normally when a device changes configuration, the entire activity is destroyed and recreated. This includes the GLSurfaceView. So far, I've only had my apps forced to landscape mode all the time. In this mode, the activity never changes orientation, so the app stays alive all the time. However, I'd like to change it to Sensor mode so that it will change orientation with the device. I'm also considering placing admob advertisements on my app. These are Android specific and can't be included inside the jme3 app, so I need to let Android deal with the orientation change. The problem with onConfigurationChange is that the activity is not destroyed and recreated, so the layout of any non-jme entities will have to be handled via code instead of the normal XML definition way.

fyi, i have a admob view for the demo of my game.
It's pretty straight forward to integrate. I place it on top of the gl surface at the top of the screen by adding it to the framelayout.
I changed the visibility of the framelayout to protected in the harness so one can do whatever he wants with the layout.

I have something running now that I’ve started to test. The basic idea is to seperate the surfaceview from the input handling so that the surfaceview can be destroyed and recreated on config changes without affect the game itself.



For now, I’m using the onRetainNonConfigurationInstance() method in harness. This is called by the system right before onDestroy and allows the activity to return back an object that is maintained and available in the next onCreate (after the screen is recreated after the config change). In this method, I am saving a reference to the app and then attaching the surfaceview to the app. This way the surfaceview and the screenlayout can be managed normally using the standard activity lifecycle leaving the game in its previous state.



I haven’t tested much yet, but it seems to be working in my game. Preliminary diff files are below, any comments are appreciated.



Summary of changes are:


  • 1) In MainActivity, use Sensor orientation to allow the game to change orientations based on the sensor feedback

  • 2) New class that only includes the GLSurfaceView called "AndroidGLSurfaceView"

  • 3) Modified "AndroidInput" to only include the input handling between the engine and the current GLSurfaceView

  • 4) Modified "OGLESContext" to create a new GLSurfaceView in CreateView and handle attaching the current GLSurfaceView to AndroidInput

  • 5) Modified "AndroidHarness" to retain the app during a config change using onRetainNonConfigurationInstance() and skip the app.destroy in onDestroy if the device is performing a config change



MainActivity:
[java]screenOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR;[/java]

AndroidGLSurfaceView:
[java]
package com.jme3.renderer.android;

import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import java.util.logging.Logger;

/**
* <code>AndroidGLSurfaceView</code> is derived from GLSurfaceView
* @author iwgeric
*
*/
public class AndroidGLSurfaceView extends GLSurfaceView {

private final static Logger logger = Logger.getLogger(AndroidGLSurfaceView.class.getName());

public AndroidGLSurfaceView(Context ctx, AttributeSet attribs) {
super(ctx, attribs);
}

public AndroidGLSurfaceView(Context ctx) {
super(ctx);
}


}
[/java]

AndroidInput:
[java]
# This patch file was generated by NetBeans IDE
# Following Index: paths are relative to: D:UserspotterecDocumentsjMonkeyProjectsjME3srcandroidcomjme3inputandroid
# This patch can be applied using context Tools: Patch action on respective folder.
# It uses platform neutral UTF-8 encoding and n newlines.
# Above lines and this line are ignored by the patching process.
Index: AndroidInput.java
--- AndroidInput.java Base (BASE)
+++ AndroidInput.java Locally Modified (Based On LOCAL)
@@ -1,12 +1,10 @@
package com.jme3.input.android;

-import android.content.Context;
-import android.opengl.GLSurfaceView;
-import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
+import android.view.View;
import com.jme3.input.KeyInput;
import com.jme3.input.RawInputListener;
import com.jme3.input.TouchInput;
@@ -25,8 +23,10 @@
* @author larynx
*
*/
-public class AndroidInput extends GLSurfaceView implements
+public class AndroidInput implements
TouchInput,
+ View.OnTouchListener,
+ View.OnKeyListener,
GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener,
ScaleGestureDetector.OnScaleGestureListener {
@@ -44,6 +44,7 @@
final private RingBuffer<TouchEvent> eventPool = new RingBuffer<TouchEvent>(MAX_EVENTS);
final private HashMap<Integer, Vector2f> lastPositions = new HashMap<Integer, Vector2f>();
// Internal
+ private View view;
private ScaleGestureDetector scaledetector;
private GestureDetector detector;
private int lastX;
@@ -149,17 +150,16 @@
0x0,//mute
};

- public AndroidInput(Context ctx, AttributeSet attribs) {
- super(ctx, attribs);
+ public AndroidInput(View view) {
+ setView(view);
detector = new GestureDetector(null, this, null, false);
- scaledetector = new ScaleGestureDetector(ctx, this);
-
+ scaledetector = new ScaleGestureDetector(view.getContext(), this);
}

- public AndroidInput(Context ctx) {
- super(ctx);
- detector = new GestureDetector(null, this, null, false);
- scaledetector = new ScaleGestureDetector(ctx, this);
+ public void setView(View view) {
+ this.view = view;
+ this.view.setOnTouchListener(this);
+ this.view.setOnKeyListener(this);
}

private TouchEvent getNextFreeTouchEvent() {
@@ -218,10 +218,12 @@
}

/**
- * onTouchEvent gets called from android thread on touchpad events
+ * onTouch gets called from android thread on touchpad events
*/
- @Override
- public boolean onTouchEvent(MotionEvent event) {
+ public boolean onTouch(View view, MotionEvent event) {
+ if (view != this.view) {
+ return false;
+ }
boolean bWasHandled = false;
TouchEvent touch;
// System.out.println("native : " + event.getAction());
@@ -236,7 +238,7 @@
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
touch = getNextFreeTouchEvent();
- touch.set(Type.DOWN, event.getX(pointerIndex), this.getHeight() - event.getY(pointerIndex), 0, 0);
+ touch.set(Type.DOWN, event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex), 0, 0);
touch.setPointerId(pointerId);
touch.setTime(event.getEventTime());
touch.setPressure(event.getPressure(pointerIndex));
@@ -248,7 +250,7 @@
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
touch = getNextFreeTouchEvent();
- touch.set(Type.UP, event.getX(pointerIndex), this.getHeight() - event.getY(pointerIndex), 0, 0);
+ touch.set(Type.UP, event.getX(pointerIndex), view.getHeight() - event.getY(pointerIndex), 0, 0);
touch.setPointerId(pointerId);
touch.setTime(event.getEventTime());
touch.setPressure(event.getPressure(pointerIndex));
@@ -261,16 +263,16 @@
for (int p = 0; p < event.getPointerCount(); p++) {
Vector2f lastPos = lastPositions.get(p);
if (lastPos == null) {
- lastPos = new Vector2f(event.getX(p), this.getHeight() - event.getY(p));
+ lastPos = new Vector2f(event.getX(p), view.getHeight() - event.getY(p));
lastPositions.put(event.getPointerId(p), lastPos);
}
touch = getNextFreeTouchEvent();
- touch.set(Type.MOVE, event.getX(p), this.getHeight() - event.getY(p), event.getX(p) - lastPos.x, this.getHeight() - event.getY(p) - lastPos.y);
+ touch.set(Type.MOVE, event.getX(p), view.getHeight() - event.getY(p), event.getX(p) - lastPos.x, view.getHeight() - event.getY(p) - lastPos.y);
touch.setPointerId(event.getPointerId(p));
touch.setTime(event.getEventTime());
touch.setPressure(event.getPressure(p));
processEvent(touch);
- lastPos.set(event.getX(p), this.getHeight() - event.getY(p));
+ lastPos.set(event.getX(p), view.getHeight() - event.getY(p));
}
bWasHandled = true;
break;
@@ -286,8 +288,15 @@
return bWasHandled;
}

- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
+ /**
+ * onKey gets called from android thread on key events
+ */
+ public boolean onKey(View view, int keyCode, KeyEvent event) {
+ if (view != this.view) {
+ return false;
+ }
+
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
TouchEvent evt;
evt = getNextFreeTouchEvent();
evt.set(TouchEvent.Type.KEY_DOWN);
@@ -304,10 +313,7 @@
} else {
return true;
}
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
TouchEvent evt;
evt = getNextFreeTouchEvent();
evt.set(TouchEvent.Type.KEY_UP);
@@ -324,7 +330,10 @@
} else {
return true;
}
+ } else {
+ return false;
}
+ }

public void loadSettings(AppSettings settings) {
mouseEventsEnabled = settings.isEmulateMouse();
@@ -401,13 +410,13 @@

if (mouseEventsEnabled) {
if (mouseEventsInvertX) {
- newX = this.getWidth() - (int) event.getX();
+ newX = view.getWidth() - (int) event.getX();
} else {
newX = (int) event.getX();
}

if (mouseEventsInvertY) {
- newY = this.getHeight() - (int) event.getY();
+ newY = view.getHeight() - (int) event.getY();
} else {
newY = (int) event.getY();
}
@@ -476,7 +485,7 @@

public void onLongPress(MotionEvent event) {
TouchEvent touch = getNextFreeTouchEvent();
- touch.set(Type.LONGPRESSED, event.getX(), this.getHeight() - event.getY(), 0f, 0f);
+ touch.set(Type.LONGPRESSED, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
@@ -484,7 +493,7 @@

public boolean onFling(MotionEvent event, MotionEvent event2, float vx, float vy) {
TouchEvent touch = getNextFreeTouchEvent();
- touch.set(Type.FLING, event.getX(), this.getHeight() - event.getY(), vx, vy);
+ touch.set(Type.FLING, event.getX(), view.getHeight() - event.getY(), vx, vy);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
@@ -499,7 +508,7 @@

public boolean onDoubleTap(MotionEvent event) {
TouchEvent touch = getNextFreeTouchEvent();
- touch.set(Type.DOUBLETAP, event.getX(), this.getHeight() - event.getY(), 0f, 0f);
+ touch.set(Type.DOUBLETAP, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
@@ -525,7 +534,7 @@

public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
TouchEvent touch = getNextFreeTouchEvent();
- touch.set(Type.SCALE_MOVE, scaleGestureDetector.getFocusX(), this.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
+ touch.set(Type.SCALE_MOVE, scaleGestureDetector.getFocusX(), view.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(scaleGestureDetector.getEventTime());
touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
@@ -538,7 +547,7 @@

public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) {
TouchEvent touch = getNextFreeTouchEvent();
- touch.set(Type.SCALE_END, scaleGestureDetector.getFocusX(), this.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
+ touch.set(Type.SCALE_END, scaleGestureDetector.getFocusX(), view.getHeight() - scaleGestureDetector.getFocusY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(scaleGestureDetector.getEventTime());
touch.setScaleSpan(scaleGestureDetector.getCurrentSpan());
@@ -548,7 +557,7 @@

public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
TouchEvent touch = getNextFreeTouchEvent();
- touch.set(Type.SCROLL, e1.getX(), this.getHeight() - e1.getY(), distanceX, distanceY * (-1));
+ touch.set(Type.SCROLL, e1.getX(), view.getHeight() - e1.getY(), distanceX, distanceY * (-1));
touch.setPointerId(0);
touch.setTime(e1.getEventTime());
processEvent(touch);
@@ -558,7 +567,7 @@

public void onShowPress(MotionEvent event) {
TouchEvent touch = getNextFreeTouchEvent();
- touch.set(Type.SHOWPRESS, event.getX(), this.getHeight() - event.getY(), 0f, 0f);
+ touch.set(Type.SHOWPRESS, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
@@ -566,7 +575,7 @@

public boolean onSingleTapUp(MotionEvent event) {
TouchEvent touch = getNextFreeTouchEvent();
- touch.set(Type.TAP, event.getX(), this.getHeight() - event.getY(), 0f, 0f);
+ touch.set(Type.TAP, event.getX(), view.getHeight() - event.getY(), 0f, 0f);
touch.setPointerId(0);
touch.setTime(event.getEventTime());
processEvent(touch);
@@ -623,4 +632,5 @@
public boolean isSimulateMouse() {
return mouseEventsEnabled;
}
+
}
[/java]

OGLESContext:
[java]
# This patch file was generated by NetBeans IDE
# Following Index: paths are relative to: D:UserspotterecDocumentsjMonkeyProjectsjME3srcandroidcomjme3systemandroid
# This patch can be applied using context Tools: Patch action on respective folder.
# It uses platform neutral UTF-8 encoding and n newlines.
# Above lines and this line are ignored by the patching process.
Index: OGLESContext.java
--- OGLESContext.java Base (BASE)
+++ OGLESContext.java Locally Modified (Based On LOCAL)
@@ -33,7 +33,6 @@

import android.app.AlertDialog;
import android.content.DialogInterface;
-import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.text.InputType;
import android.view.Gravity;
@@ -46,6 +45,7 @@
import com.jme3.input.controls.SoftTextDialogInputListener;
import com.jme3.input.dummy.DummyKeyInput;
import com.jme3.input.dummy.DummyMouseInput;
+import com.jme3.renderer.android.AndroidGLSurfaceView;
import com.jme3.renderer.android.OGLESShaderRenderer;
import com.jme3.system.*;
import com.jme3.system.android.AndroidConfigChooser.ConfigType;
@@ -73,7 +73,8 @@
protected Timer timer;
protected SystemListener listener;
protected boolean autoFlush = true;
- protected AndroidInput view;
+ protected AndroidInput androidInput;
+ protected AndroidGLSurfaceView view;
protected int minFrameDuration = 0; // No FPS cap
/**
* EGL_RENDERABLE_TYPE: EGL_OPENGL_ES_BIT = OpenGL ES 1.0 |
@@ -107,7 +108,12 @@
public GLSurfaceView createView(ConfigType configType, boolean eglConfigVerboseLogging) {

// Start to set up the view
- this.view = new AndroidInput(JmeAndroidSystem.getActivity());
+ view = new AndroidGLSurfaceView(JmeAndroidSystem.getActivity());
+ if (androidInput == null) {
+ androidInput = new AndroidInput(view);
+ } else {
+ androidInput.setView(view);
+ }
if (configType == ConfigType.LEGACY) {
// Hardcoded egl setup
clientOpenGLESVersion = 2;
@@ -166,6 +172,7 @@
// renderer:initialize
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig cfg) {
+ logger.info("onSurfaceCreated");
if (created.get() && renderer != null) {
renderer.resetGLObjects();
} else {
@@ -268,7 +275,7 @@

@Override
public TouchInput getTouchInput() {
- return view;
+ return androidInput;
}

@Override
[/java]

AndroidHarness:
[java]
# This patch file was generated by NetBeans IDE
# Following Index: paths are relative to: D:UserspotterecDocumentsjMonkeyProjectsjME3srcandroidcomjme3app
# This patch can be applied using context Tools: Patch action on respective folder.
# It uses platform neutral UTF-8 encoding and n newlines.
# Above lines and this line are ignored by the patching process.
Index: AndroidHarness.java
--- AndroidHarness.java Base (BASE)
+++ AndroidHarness.java Locally Modified (Based On LOCAL)
@@ -137,12 +137,49 @@
protected FrameLayout frameLayout = null;
final private String ESCAPE_EVENT = "TouchEscape";
private boolean firstDrawFrame = true;
+ private boolean inConfigChange = false;

+ private class DataObject {
+ protected Application app = null;
+ }
+
@Override
+ public Object onRetainNonConfigurationInstance() {
+ logger.log(Level.INFO, "onRetainNonConfigurationInstance called");
+ final DataObject data = new DataObject();
+ data.app = this.app;
+ inConfigChange = true;
+ logger.log(Level.INFO, "app: {0}", app.hashCode());
+ logger.log(Level.INFO, "ctx: {0}", ctx.hashCode());
+ logger.log(Level.INFO, "view: {0}", view.hashCode());
+ logger.log(Level.INFO, "isGLThreadPaused: {0}", isGLThreadPaused);
+ logger.log(Level.INFO, "inConfigChange: {0}", inConfigChange);
+ return data;
+ }
+
+ @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

JmeAndroidSystem.setActivity(this);
+
+ final DataObject data = (DataObject) getLastNonConfigurationInstance();
+ if (data != null) {
+ logger.log(Level.INFO, "onCreate: onRetainNonConfigurationInstance is not null");
+ this.app = data.app;
+
+ ctx = (OGLESContext) app.getContext();
+ view = ctx.createView(eglConfigType, eglConfigVerboseLogging);
+ ctx.setSystemListener(this);
+ layoutDisplay();
+
+ logger.log(Level.INFO, "app: {0}", app.hashCode());
+ logger.log(Level.INFO, "ctx: {0}", ctx.hashCode());
+ logger.log(Level.INFO, "view: {0}", view.hashCode());
+
+ } else {
+ logger.log(Level.INFO, "onCreate: onRetainNonConfigurationInstance is null");
+
if (screenFullScreen) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
@@ -188,6 +225,7 @@
setContentView(new TextView(this));
}
}
+ }

@Override
protected void onRestart() {
@@ -210,6 +248,8 @@
super.onResume();
if (view != null) {
view.onResume();
+ } else {
+ logger.info("view is null");
}

if (app != null) {
@@ -258,9 +298,14 @@

@Override
protected void onDestroy() {
+ final DataObject data = (DataObject) getLastNonConfigurationInstance();
+ if (data != null || inConfigChange) {
+ logger.info("onDestroy: found DataObject or inConfigChange");
+ } else {
if (app != null) {
app.stop(!isGLThreadPaused);
}
+ }
logger.info("onDestroy");
super.onDestroy();
}
@@ -362,7 +407,14 @@
splashImageView.setImageResource(splashPicID);
}

+ if (view.getParent() != null) {
+ ((ViewGroup)view.getParent()).removeView(view);
+ }
frameLayout.addView(view);
+
+ if (splashImageView.getParent() != null) {
+ ((ViewGroup)splashImageView.getParent()).removeView(splashImageView);
+ }
frameLayout.addView(splashImageView, lp);

setContentView(frameLayout);
@@ -396,6 +448,7 @@
}

public void initialize() {
+ logger.info("initialize");
app.initialize();
if (handleExitHook) {
app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
@@ -404,25 +457,30 @@
}

public void reshape(int width, int height) {
+ logger.info("reshape");
app.reshape(width, height);
}

public void update() {
+// logger.info("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) {
+// logger.info("firstDrawFrame");
removeSplashScreen();
firstDrawFrame = false;
}
}

public void requestClose(boolean esc) {
+ logger.info("requestClose");
app.requestClose(esc);
}

public void destroy() {
+ logger.info("destroy");
if (app != null) {
app.destroy();
}
@@ -432,12 +490,14 @@
}

public void gainFocus() {
+ logger.info("gainFocus");
if (app != null) {
app.gainFocus();
}
}

public void loseFocus() {
+ logger.info("loseFocus");
if (app != null) {
app.loseFocus();
}
[/java]
1 Like

@iwgeric: I applied your patch but I encountered a couple of issues:



  • What's the point of "inConfigChange" variable if its only set to true and never to false? Why does it assume a configuration change from that point on if its not?


  • How come the title bar and full screen modes aren't applied on configuration changes (only on initial app launch)?

@Momoko_Fan said:
What's the point of "inConfigChange" variable if its only set to true and never to false? Why does it assume a configuration change from that point on if its not?


The inConfigChange is set false when AndroidHarness is created and then only set true when the Android system calls onRetainNonConfigurationInstance. This means that the boolean is true only between the call of onRetainNonConfigurationInstance and the activity being destroyed. This is what is used to skip destroying the app. If there is no configuration change, the Android system will not call onRetainNonConfigurationInstance and the boolean will remain false allowing the app to be destroyed in the onDestroy method. AndroidHarness is recreated from scratch after the config change so the boolean is set back to false until the next config change.

@Momoko_Fan said:
How come the title bar and full screen modes aren't applied on configuration changes (only on initial app launch)?


That looks like a mistake. I'll take a look at that. I have my app set to show the notification bar but not the title bar (full screen set to false and show title bar set to false in MainActivity). After a config change, both the notification and the title bar are shown, I'll correct that and submit a patch.

Thanks for taking a look at this.
@Momoko_Fan said:
How come the title bar and full screen modes aren't applied on configuration changes (only on initial app launch)?


Here is a new patch file for AndroidHarness. I moved the code for applying the full screen and title settings to the window to above the getLastNonConfigurationInstance(); call.

Also down below is what the onCreate should look like after this patch is applied.

[java]
# This patch file was generated by NetBeans IDE
# Following Index: paths are relative to: D:UserspotterecDocumentsjMonkeyProjectsjME3srcandroidcomjme3app
# This patch can be applied using context Tools: Patch action on respective folder.
# It uses platform neutral UTF-8 encoding and n newlines.
# Above lines and this line are ignored by the patching process.
Index: AndroidHarness.java
--- AndroidHarness.java Base (BASE)
+++ AndroidHarness.java Locally Modified (Based On LOCAL)
@@ -137,8 +137,27 @@
protected FrameLayout frameLayout = null;
final private String ESCAPE_EVENT = "TouchEscape";
private boolean firstDrawFrame = true;
+ private boolean inConfigChange = false;

+ private class DataObject {
+ protected Application app = null;
+ }
+
@Override
+ public Object onRetainNonConfigurationInstance() {
+ logger.log(Level.INFO, "onRetainNonConfigurationInstance called");
+ final DataObject data = new DataObject();
+ data.app = this.app;
+ inConfigChange = true;
+ logger.log(Level.INFO, "app: {0}", app.hashCode());
+ logger.log(Level.INFO, "ctx: {0}", ctx.hashCode());
+ logger.log(Level.INFO, "view: {0}", view.hashCode());
+ logger.log(Level.INFO, "isGLThreadPaused: {0}", isGLThreadPaused);
+ logger.log(Level.INFO, "inConfigChange: {0}", inConfigChange);
+ return data;
+ }
+
+ @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

@@ -155,6 +174,24 @@

setRequestedOrientation(screenOrientation);

+
+ final DataObject data = (DataObject) getLastNonConfigurationInstance();
+ if (data != null) {
+ logger.log(Level.INFO, "onCreate: onRetainNonConfigurationInstance is not null");
+ this.app = data.app;
+
+ ctx = (OGLESContext) app.getContext();
+ view = ctx.createView(eglConfigType, eglConfigVerboseLogging);
+ ctx.setSystemListener(this);
+ layoutDisplay();
+
+ logger.log(Level.INFO, "app: {0}", app.hashCode());
+ logger.log(Level.INFO, "ctx: {0}", ctx.hashCode());
+ logger.log(Level.INFO, "view: {0}", view.hashCode());
+
+ } else {
+ logger.log(Level.INFO, "onCreate: onRetainNonConfigurationInstance is null");
+
// Create Settings
AppSettings settings = new AppSettings(true);
settings.setEmulateMouse(mouseEventsEnabled);
@@ -188,6 +225,7 @@
setContentView(new TextView(this));
}
}
+ }

@Override
protected void onRestart() {
@@ -210,6 +248,8 @@
super.onResume();
if (view != null) {
view.onResume();
+ } else {
+ logger.info("view is null");
}

if (app != null) {
@@ -258,9 +298,14 @@

@Override
protected void onDestroy() {
+ final DataObject data = (DataObject) getLastNonConfigurationInstance();
+ if (data != null || inConfigChange) {
+ logger.info("onDestroy: found DataObject or inConfigChange");
+ } else {
if (app != null) {
app.stop(!isGLThreadPaused);
}
+ }
logger.info("onDestroy");
super.onDestroy();
}
@@ -362,7 +407,14 @@
splashImageView.setImageResource(splashPicID);
}

+ if (view.getParent() != null) {
+ ((ViewGroup)view.getParent()).removeView(view);
+ }
frameLayout.addView(view);
+
+ if (splashImageView.getParent() != null) {
+ ((ViewGroup)splashImageView.getParent()).removeView(splashImageView);
+ }
frameLayout.addView(splashImageView, lp);

setContentView(frameLayout);
@@ -396,6 +448,7 @@
}

public void initialize() {
+ logger.info("initialize");
app.initialize();
if (handleExitHook) {
app.getInputManager().addMapping(ESCAPE_EVENT, new TouchTrigger(TouchInput.KEYCODE_BACK));
@@ -404,25 +457,30 @@
}

public void reshape(int width, int height) {
+ logger.info("reshape");
app.reshape(width, height);
}

public void update() {
+// logger.info("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) {
+// logger.info("firstDrawFrame");
removeSplashScreen();
firstDrawFrame = false;
}
}

public void requestClose(boolean esc) {
+ logger.info("requestClose");
app.requestClose(esc);
}

public void destroy() {
+ logger.info("destroy");
if (app != null) {
app.destroy();
}
@@ -432,12 +490,14 @@
}

public void gainFocus() {
+ logger.info("gainFocus");
if (app != null) {
app.gainFocus();
}
}

public void loseFocus() {
+ logger.info("loseFocus");
if (app != null) {
app.loseFocus();
}
[/java]


Full onCreate() method:
[java]
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

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);


final DataObject data = (DataObject) getLastNonConfigurationInstance();
if (data != null) {
logger.log(Level.INFO, "onCreate: onRetainNonConfigurationInstance is not null");
this.app = data.app;

ctx = (OGLESContext) app.getContext();
view = ctx.createView(eglConfigType, eglConfigVerboseLogging);
ctx.setSystemListener(this);
layoutDisplay();

logger.log(Level.INFO, "app: {0}", app.hashCode());
logger.log(Level.INFO, "ctx: {0}", ctx.hashCode());
logger.log(Level.INFO, "view: {0}", view.hashCode());

} else {
logger.log(Level.INFO, "onCreate: onRetainNonConfigurationInstance is null");

// Create Settings
AppSettings settings = new AppSettings(true);
settings.setEmulateMouse(mouseEventsEnabled);
settings.setEmulateMouseFlipAxis(mouseEventsInvertX, mouseEventsInvertY);

// 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(eglConfigType, eglConfigVerboseLogging);

// Set the screen reolution
//TODO try to find a better way to get a hand on the resolution
WindowManager wind = this.getWindowManager();
Display disp = wind.getDefaultDisplay();
logger.log(Level.WARNING, "Resolution from Window: {0}, {1}", new Object[]{disp.getWidth(), disp.getHeight()});
ctx.getSettings().setResolution(disp.getWidth(), disp.getHeight());

// AndroidHarness wraps the app as a SystemListener.
ctx.setSystemListener(this);
layoutDisplay();
} catch (Exception ex) {
handleError("Class " + appClass + " init failed", ex);
setContentView(new TextView(this));
}
}
}
[/java]
3 Likes

@Momoko_Fan

Everything seems to be working with the last patch applied. Let me know if you see anything else that needs to be addressed.



@nehon

Do you think there anything else that should be included / modified while I’m mucking around with AndroidHarness?

@Momoko_Fan @nehon

Is there anything else I need to do to prepare this for being committed by someone?

@Momoko_Fan what do you think.

Imo it’s fine.

It has been committed, although I did not test it yet.

@Momoko_Fan thanks for committing it. When you look at it, there are some log prints in onRetainNonConfigurationInstance and onCreate that can probably be removed. Thanks again.