Game quits when rotating phone screen on Android
Description
A Python Panda3D program compiled for Android dies when the phone is rotated. This seems to happen irrespective of any values specified in android:configChanged in AndroidManifest.xml. There is mention in the log file of an exception happening in the application thread but the exception itself is not shown (perhaps it'd be good to solve that issue first).
Steps to Reproduce
- Compile the Asteroids sample for Android using deploy-ng:
python setup.py bdist_apps
- Extract an apks file and install it on a phone:
bundletool build-apks --ks-key-alias androiddebugkey --ks-pass pass:android --ks ../../debug.ks --verbose --bundle dist/asteroids-0.0.0_android_arm64.aab --output asteroids.apks
bundletool install-apks --apks asteroids.apks
- Run the application and rotate it.
Log output from logcat (filtered to show only relevant messages) is:
V/threaded_app(24859): Creating: 0x7f7de2f500
V/threaded_app(24859): Config: mcc=204 mnc=8 lang=en cnt=GB orien=1 touch=3 dens=320 keys=1 nav=1 keysHid=3 navHid=0 sdk=25 size=2 long=1 modetype=1 modenight=1
I/Panda3D (24859): :android: New native activity started on ExternalThread Thread-2
V/threaded_app(24859): Start: 0x7f7de2f500
I/Panda3D (24859): :android: Path to data: /data/user/0/name.rdb.panda3d.samples.asteroids
I/Panda3D (24859): :android: Path to cache: /data/user/0/name.rdb.panda3d.samples.asteroids/cache
I/Panda3D (24859): :android: Path to APK: /data/app/name.rdb.panda3d.samples.asteroids-2/base.apk
I/Panda3D (24859): :android: Path to native library: /data/app/name.rdb.panda3d.samples.asteroids-2/lib/arm64/libasteroids.so
I/Panda3D (24859): Known pipe types:
I/Panda3D (24859): AndroidGraphicsPipe
I/Panda3D (24859): (1 aux display modules not yet loaded.)
V/threaded_app(24859): activityState=10
V/threaded_app(24859): Resume: 0x7f7de2f500
V/threaded_app(24859): activityState=11
V/threaded_app(24859): InputQueueCreated: 0x7f7de2f500 -- 0x7f7de33600
V/threaded_app(24859): APP_CMD_INPUT_CHANGED
V/threaded_app(24859): Attaching input queue to looper
I/InputDispatcher( 2195): Focus entered window: Window{926a318 u0 name.rdb.panda3d.samples.asteroids/org.panda3d.android.PandaActivity}
V/threaded_app(24859): NativeWindowCreated: 0x7f7de2f500 -- 0x7f7eebc610
V/threaded_app(24859): APP_CMD_INIT_WINDOW
V/threaded_app(24859): NativeWindowResized: 0x7f7de2f500 -- 0x7f7eebc610
V/threaded_app(24859): ContentRectChanged: l=0,t=48,r=720,b=1184
V/threaded_app(24859): WindowFocusChanged: 0x7f7de2f500 -- 1
E/Panda3D (24859): :device(error): Error adding inotify watch on /dev/input: Permission denied
E/Panda3D (24859): :device(error): Error opening directory /dev/input: Permission denied
V/threaded_app(24859): NativeWindowDestroyed: 0x7f7de2f500 -- 0x7f7eebc610
E/Panda3D (24859): :display:gsg:gles2gsg(error): program %d is not a program object or shader %d is not a shader object
E/Panda3D (24859): :display:gsg:gles2gsg(error): program %d is not a program object or shader %d is not a shader object
E/Panda3D (24859): :display:gsg:gles2gsg(error): program %d is not a program object
E/Panda3D (24859): :display:gsg:gles2gsg(error): shader %d is not a shader object
E/Panda3D (24859): :display:gsg:gles2gsg(error): shader %d is not a shader object
V/threaded_app(24859): Pause: 0x7f7de2f500
V/threaded_app(24859): activityState=13
V/threaded_app(24859): SaveInstanceState: 0x7f7de2f500
V/threaded_app(24859): APP_CMD_SAVE_STATE
V/threaded_app(24859): Stop: 0x7f7de2f500
V/threaded_app(24859): activityState=14
V/threaded_app(24859): NativeWindowDestroyed: 0x7f7de2f500 -- 0x7f7eebd410
V/threaded_app(24859): APP_CMD_TERM_WINDOW
V/threaded_app(24859): APP_CMD_TERM_WINDOW
V/threaded_app(24859): InputQueueDestroyed: 0x7f7de2f500 -- 0x7f7de33600
V/threaded_app(24859): APP_CMD_INPUT_CHANGED
V/threaded_app(24859): Destroy: 0x7f7de2f500
V/threaded_app(24859): APP_CMD_DESTROY
E/Panda3D (24859): :thread(error): Exception occurred within ExternalThread Thread-2
I/ActivityManager( 2195): Process name.rdb.panda3d.samples.asteroids (pid 24859) has died
D/ActivityManager( 2195): cleanUpApplicationRecord -- 24859
W/ActivityManager( 2195): Force removing ActivityRecord{93f8601 u0 name.rdb.panda3d.samples.asteroids/org.panda3d.android.PandaActivity t130}: app died, no saved state
Environment
- Operating system: Android 7.1.1
- System architecture: arm64-v8a
- Panda3D version: 429acd6e0d0762192dd95c15a46251ca244356b4
- Installation method: built from source
- Python version (if using Python): 3.8
There are several issues adding up to make this happen:
- [x] Exceptions are not being printed properly (fixed in b8c301164a5c2449a70d8a0381d76ee26f5482c0)
- [x] The process is quitting immediately on a
SystemExitin a thread because of odd behaviour byPyErr_Print()(worked around in cc865e6d21a508754646bd79341edd1d0784caa3) - [x] Android is completely recreating the app with
onDestroy()andonCreate()even though havingandroid:configChanges="orientation|screenSize|screenLayout"in the<activity>definition should prevent this - [ ] Android's NativeActivity isn't calling
dlclose()on the native library inonDestroy() - [ ] Python isn't calling
dlclose()on loaded modules inPy_Finalize()either, so it causes problems when - [ ] Panda stores some Python objects with static storage duration which will cause crashes in the interpreter when it restarts without the Python modules being reloaded
In my opinion, there are two ways to go about this:
- We patch NativeActivity to somehow dlclose the library, or we load the Python interpreter indirectly through another .so that we can dlclose later, so that everything truly gets unloaded from memory by
onDestroy(). The problem remains that the application effectively restarts from scratch on orientation change unless the user implements a custom state saving / restoring mechanism. - We don't stop the interpreter at all in
onDestroy()(or perhaps if we somehow detect that it's not a "true" destruction), but rather just stop the task manager, retaining all the game state. The nextonCreate()just restarts the task manager. Given a lack of ability to truly "save state" in Panda, this might be the best way to deal with these kinds of restarts, although it's clearly not how Android wants apps to handle this, and we'd need to tossandroid_native_app_gluein favour of our own lifecycle management (also because we would need to init Python in the main thread, since it might not like its "main thread" disappearing).
As a workaround for now is there a way to lock the orientation to landscape (or portrait) on boot?
Yes, you can set that in the AndroidManifest.xml
Looking into this a bit, I found the following StackOverflow thread, showing a similar issue with the app being recreated despite setting "configChanges": https://stackoverflow.com/questions/8883482/activity-still-recreated-with-configchanges
There someone notes that:
The emulator emulates a device with a side-slider keyboard. The android:configChanges value that matches your - would be keyboardHidden, generally used in conjunction with orientation to handle non-keyboard devices (e.g., android:configChanges="keyboardHidden|orientation").
Might that addition--or some other change that is happening in conjunction with the change in orientation--be the cause of Android still destroying and recreating the app here?
To the broader question--because I gather that there are other changes that could prompt this destruction-and-recreation behaviour--the only real solution that I see offhand is for the developer to save their state when the destruction is ordered, and then load that state when the recreation is enacted.
After all, it's not just Panda's state that the user would likely expect to see restored, but the full game-state.
Maybe Panda could nudge developers to this, and make life slightly easier for them, by implementing the running of callback functions for saving and loading that the developer is expected to override...?
From my experience, adding lots of things to configChanges didn't seem to have an effect on Android restarting the app, which is unfortunate since that would basically solve our problem. But that was a long time ago, and feel free to experiment further.
Of course we definitely should offer developers hooks to save and restore state of their game, but if we can avoid this from happening when the window is rotated (which is something Panda can handle just fine) that would be great.
As for how to modify the AndroidManifest.xml (I saw a question about that), you can for now modify the Python code that generates it in direct/dist/commands.py of the Panda3D installation (function generate_android_manifest). In the long term we need a way to specify overrides for this in the setup.py.
From my experience, adding lots of things to configChanges didn't seem to have an effect on Android restarting the app, which is unfortunate since that would basically solve our problem.
Hmm, that is a pity!
Is there no way to have Android report what changes are prompting these restarts? That way we could perhaps build a full list of things to add to "configChanges"--maybe there's still something missing there...
Of course we definitely should offer developers hooks to save and restore state of their game, but if we can avoid this from happening when the window is rotated (which is something Panda can handle just fine) that would be great.
This is true!
I found out why my configChanges alterations weren't making a difference: a bug in the code that built a binary version of the .xml file when building the .aab that took only the last flag instead of OR-ing all the flags together. 🤦 At least on the emulator, orientation changes (and other changes like locale, font scale, grammatical gender) no longer cause the game to restart now.
I also found out that forcibly exiting the process upon activity destruction forces Android to recreate the process when a configuration change does cause an app reload, which actually seems to be an effective workaround for now. Of course, we will still need to have some event that the app can respond to to save and restore the application state, and events so that the app can respond to configuration changes like font scale, user gender, what have you.
I have also been doing other work on the Android port, such as updating it for Python 3.13, which has much-improved Android support, and fixing some regressions that crept into the master branch. I would recommend people to use Python 3.13 for Android going forward.