Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jnius can't find class "org.kivy.android.PythonActivity" with webview #2533

Closed
4 of 5 tasks
foolish-suits opened this issue Dec 27, 2021 · 5 comments
Closed
4 of 5 tasks

Comments

@foolish-suits
Copy link

foolish-suits commented Dec 27, 2021

Checklist

  • the issue is indeed a bug and not a support request
  • issue doesn't already exist: https://github.com/kivy/python-for-android/issues
  • I have a short, runnable example that reproduces the issue
  • I reproduced the problem with the latest development version (p4a.branch = develop)
  • I used the grave accent (aka backticks) to format code or logs when appropriated

Versions

  • Python: 3.8.10
  • OS: Ubuntu:20.04
  • Kivy: 2.0.0
  • Cython: 0.29.15
  • OpenJDK: java-13-openjdk-amd64

I followed the build env from the Docker in version v2021.09.05.

Description

I want to use webview as bootstrap and use jnius to access Android API.
I tried the unit test code but build with webview as bootstrap. Build success, running and show the web well, but "vibrate" function failed with 'Didn't find class "org.kivy.android.PythonActivity"'
If I build with SDL2 the vibrate function will work well.

I have tried a lots but no luck, is there any demo or example can work as webview+jnius? Any advice are appreciate.
Thank you!

Command:

PYTHONPATH=. python3 -m pythonforandroid.toolchain apk  --private=/home/user/app/testapps/on_device_unit_tests/test_app --dist-name=testapp --package=org.tp4a --name=TestApp --version=0.1 --requirements=sqlite3,libffi,openssl,pyjnius,flask,python3,genericndkbuild --bootstrap=webview --ndk-dir=/home/user/.android/android-ndk-r19b --sdk-dir=/home/user/.android/android-sdk --android-api=27 --arch=armeabi-v7a --permission=INTERNET --permission=VIBRATE  --debug --ignore-setup-py --storage-dir=/home/user/.local/share/python-for-android


Logs

I python : File "jnius/jnius_export_func.pxi", line 26, in jnius.jnius.find_javaclass
I python : File "jnius/jnius_utils.pxi", line 91, in jnius.jnius.check_exception
I python : jnius.jnius.JavaException: JVM exception occurred: Didn't find class "org.kivy.android.PythonActivity" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib, /vendor/lib, /system/lib, /vendor/lib]] java.lang.ClassNotFoundException

@dbnicholson
Copy link
Contributor

I was able to reproduce this with the on_device_tests test app by building it with the webview bootstrap. You can trigger it by either running the unit tests or trying to have it vibrate. Both try to load the PythonActivity class and fail.

However, if I load it very early in the python app (even if I don't use it), then it works and continues to work later. After reading https://developer.android.com/training/articles/perf-jni#faq_FindClass, my guess is that loading the WebView changes the class loader at some point. If the desired class hasn't already been loaded from the JNI, then it has to be found using the class loader Webview creates and that fails.

@foolish-suits if you add a call to autoclass("org.kivy.android.PythonActivity") early in your main entrypoint, does it work?

@dbnicholson
Copy link
Contributor

I think a workaround is to add some FindClass calls to the JNI_OnLoad JNI method. I was reviewing the webview JNI loading code and I noticed that the SDL JNI loading code that it was copied from has evolved some since then.

One thing that does happen in the SDL code is that it registers the methods from its classes when the library is loaded. A side effect of that is that the Java classes are looked up. We could probably do something similar. I don't know if we have to go all the way to registering the methods, but I think at least adding the FindClass calls for all the classes would hopefully keep them resolved.

@dbnicholson
Copy link
Contributor

I think figured out what causes it to break. If you run flask with threaded=False, then it works. I think that indicates that the request is being handled by a new thread, but the new thread is no longer using the correct class loader. That is one of the thing's that's cautioned about in https://developer.android.com/training/articles/perf-jni#faq_FindClass:

You can get into trouble if you create a thread yourself (perhaps by calling pthread_create and then attaching it with AttachCurrentThread). Now there are no stack frames from your application. If you call FindClass from this thread, the JavaVM will start in the "system" class loader instead of the one associated with your application, so attempts to find app-specific classes will fail.

This seems to be exactly what's happening. A native thread is being created to handle each request in werkzeug. This new thread is attached to the JVM via a pyjnius monkey patch, but since it has a new stack frame you end up using the Java system class loader. I actually think this likely affects sdl2 apps as well since I don't think there's anything SDL can do to avoid this situation.

So, I think there are basically 2 solutions:

  1. Document that if you're using threads in your app, you have to resolve app classes before you run them.
  2. Resolve the classes in the JNI_OnLoad function. I think this is doable except that the build time constructed service classes would be tricky to handle. This also wouldn't affect the sdl2 bootstrap case since p4a uses it's JNI handling code.

For now I'm just going to make a PR that makes flask run non-threaded in the test app since it specifically tries to delay resolving the classes.

@Julian-O
Copy link
Contributor

Closing as resolved.

@tsiens
Copy link

tsiens commented Jan 22, 2024

I think figured out what causes it to break. If you run flask with threaded=False, then it works. I think that indicates that the request is being handled by a new thread, but the new thread is no longer using the correct class loader. That is one of the thing's that's cautioned about in https://developer.android.com/training/articles/perf-jni#faq_FindClass:

You can get into trouble if you create a thread yourself (perhaps by calling pthread_create and then attaching it with AttachCurrentThread). Now there are no stack frames from your application. If you call FindClass from this thread, the JavaVM will start in the "system" class loader instead of the one associated with your application, so attempts to find app-specific classes will fail.

This seems to be exactly what's happening. A native thread is being created to handle each request in werkzeug. This new thread is attached to the JVM via a pyjnius monkey patch, but since it has a new stack frame you end up using the Java system class loader. I actually think this likely affects sdl2 apps as well since I don't think there's anything SDL can do to avoid this situation.

So, I think there are basically 2 solutions:

  1. Document that if you're using threads in your app, you have to resolve app classes before you run them.
  2. Resolve the classes in the JNI_OnLoad function. I think this is doable except that the build time constructed service classes would be tricky to handle. This also wouldn't affect the sdl2 bootstrap case since p4a uses it's JNI handling code.

For now I'm just going to make a PR that makes flask run non-threaded in the test app since it specifically tries to delay resolving the classes.

However, I still need to use Multithreading in the Flask,I am trying to use

from android.runnable import Runnable
Runnable(main)(*args, **kwargs)

But when the request calls the Runnable, the entire app will freeze and unable to perform any operation,Until the Runnableis completed
What should I do?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants