-
Notifications
You must be signed in to change notification settings - Fork 256
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
Android webview with address bar "no attribute 'invoke'" error #575
Comments
I thought this was such an elegant solution that I tried it. I used buildozer (1.2.0) defaults, except android.permissions = INTERNET I tested on Android 11 on Pixel. Removing the address_bar code, and changing the webview height, and it still works perfectly. |
Thanks a lot for testing.
|
That looks like a memory issue. Just a guess of course. If it were me I'd try commenting the memory free code (one step at a time), in Perhaps I suggest this because I know I don't know when exactly when Java code is free'd by Java. |
I removed the address bar, |
Strangely enough, but I overcame the issue by just clearing ".buildozer" folder! Steps to reproduce the error:
After that the app crashes. Sometimes (but not always) it shows the same error as above:
Tested on Android Emulator (Pixel 3a) for Android 9-11 (with Google Play) - the app crahes. Code: from kivy.app import App
from kivy.lang import Builder
from kivy.base import EventLoop
from kivy.factory import Factory
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.uix.modalview import ModalView
from kivy.core.clipboard import Clipboard
from kivy.utils import platform
if platform == 'android':
from android.runnable import run_on_ui_thread
from jnius import autoclass, cast, PythonJavaClass, java_method
WebView = autoclass('android.webkit.WebView')
WebViewClient = autoclass('android.webkit.WebViewClient')
LayoutParams = autoclass('android.view.ViewGroup$LayoutParams')
LinearLayout = autoclass('android.widget.LinearLayout')
activity = autoclass('org.kivy.android.PythonActivity').mActivity
KeyEvent = autoclass('android.view.KeyEvent')
class AndroidWidgetHolder(Widget):
'''Source: https://github.com/tito/android-zbar-qrcode/blob/master/main.py'''
# Must be an Android View
view = ObjectProperty(allownone=True)
def __init__(self, **kwargs):
self._old_view = None
from kivy.core.window import Window
self._window = Window
kwargs['size_hint'] = (None, None)
super().__init__(**kwargs)
@run_on_ui_thread
def on_view(self, instance, view):
if self._old_view is not None:
layout = cast(LinearLayout, self._old_view.getParent())
layout.removeView(self._old_view)
self._old_view = None
if view is None:
return
activity.addContentView(view, LayoutParams(*self.size))
view.setX(self.x)
view.setY(self._window.height - self.y - self.height)
self._old_view = view
@run_on_ui_thread
def on_size(self, instance, size):
if self.view:
params = self.view.getLayoutParams()
params.width = self.width
params.height = self.height
self.view.setLayoutParams(params)
self.view.setY(self._window.height - self.y - self.height)
@run_on_ui_thread
def on_x(self, instance, x):
if self.view:
self.view.setX(x)
@run_on_ui_thread
def on_y(self, instance, y):
if self.view:
self.view.setY(self._window.height - self.y - self.height)
class KeyListener(PythonJavaClass):
__javacontext__ = 'app'
__javainterfaces__ = ['android/view/View$OnKeyListener']
def __init__(self, listener):
super().__init__()
self.listener = listener
@java_method('(Landroid/view/View;ILandroid/view/KeyEvent;)Z')
def onKey(self, v, key_code, event):
if event.getAction() == KeyEvent.ACTION_DOWN and key_code == KeyEvent.KEYCODE_BACK:
return self.listener()
class WebViewPopup(ModalView):
def __init__(self, url, **kwargs):
self.url = url
super().__init__(**kwargs)
@run_on_ui_thread
def create_webview(self):
webview = WebView(activity)
webview_settings = webview.getSettings()
webview_settings.setJavaScriptEnabled(True)
wvc = WebViewClient()
webview.setWebViewClient(wvc)
webview.setOnKeyListener(KeyListener(self.on_back_pressed))
self.ids['webview_holder'].view = webview
self.webview = webview
@run_on_ui_thread
def on_open(self):
self.create_webview()
self.webview.loadUrl(self.url)
@run_on_ui_thread
def on_dismiss(self):
self.webview.clearHistory()
self.webview.clearCache(True)
self.webview.clearFormData()
self.webview.freeMemory()
self.ids['webview_holder'].view = None
@run_on_ui_thread
def on_back_pressed(self):
if self.webview.canGoBack():
self.webview.goBack()
else:
self.dismiss()
return True
@run_on_ui_thread
def copy_url(self):
Clipboard.copy(self.webview.getUrl())
kv = '''
#:import Factory kivy.factory.Factory
FloatLayout:
Button:
size_hint: (0.9, 0.3)
pos_hint: {'x':.05, 'y':0.35}
text: 'Open webview'
on_press: Factory.WebViewPopup('https://www.google.com').open()
<WebViewPopup>:
auto_dismiss: False
BoxLayout:
orientation: 'vertical'
Button:
size_hint: (1, 0.1)
text: 'Copy'
on_press: root.copy_url()
AndroidWidgetHolder:
id: webview_holder
size_hint: (1, 0.9)
'''
class BrowserApp(App):
def build(self):
self.bind(on_start=self.post_build_init)
return Builder.load_string(kv)
def post_build_init(self, ev):
EventLoop.window.bind(on_key_down=self.hook_keyboard)
def hook_keyboard(self, win, key, scancode, codepoint, modifiers):
if key in [27, 1001]: # Back or Esc button
if platform == 'android' and isinstance(self.root_window.children[0], WebViewPopup): # Check if WebViewPopup is open
webview_popup = self.root_window.children[0]
webview_popup.on_back_pressed()
return True
app = BrowserApp()
app.run() |
Sure, "pure" webview fortunately works fine. But I'd like to add some functionality to it to make it look more like a web-browser.
In the new example there shouldn't be any clean-up after Step 4, so here it's probably not the reason. Anyway, I tried these suggestions on the new example, still fails. |
Finally got the simple WebView implementations containing the bug above (at least on Android 10).
Case 1 import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.utils import platform
from kivy.uix.widget import Widget
from kivy.uix.modalview import ModalView
from kivy.clock import Clock
from android.runnable import run_on_ui_thread
from jnius import autoclass, PythonJavaClass, java_method, cast
WebView = autoclass('android.webkit.WebView')
WebViewClient = autoclass('android.webkit.WebViewClient')
activity = autoclass('org.kivy.android.PythonActivity').mActivity
KeyEvent = autoclass('android.view.KeyEvent')
LayoutParams = autoclass('android.view.ViewGroup$LayoutParams')
ViewGroup = autoclass('android.view.ViewGroup')
class KeyListener(PythonJavaClass):
__javacontext__ = 'app'
__javainterfaces__ = ['android/view/View$OnKeyListener']
def __init__(self, listener):
super().__init__()
self.listener = listener
@java_method('(Landroid/view/View;ILandroid/view/KeyEvent;)Z')
def onKey(self, v, key_code, event):
if event.getAction() == KeyEvent.ACTION_DOWN and key_code == KeyEvent.KEYCODE_BACK:
return self.listener()
class WebViewPopup(ModalView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@run_on_ui_thread
def create_webview(self, *args):
webview = WebView(activity)
webview_settings = webview.getSettings()
webview_settings.setJavaScriptEnabled(True)
activity.addContentView(webview, LayoutParams(-1,-1))
webview.setOnKeyListener(KeyListener(self.on_back_pressed))
self.webview = webview
@run_on_ui_thread
def on_open(self):
self.create_webview()
self.webview.loadUrl('https://duckduckgo.com/')
@run_on_ui_thread
def on_dismiss(self):
self.webview.clearHistory()
self.webview.clearCache(True)
self.webview.clearFormData()
self.webview.freeMemory()
parent = cast(ViewGroup, self.webview.getParent())
if parent is not None: parent.removeView(self.webview)
self.webview = None
@run_on_ui_thread
def on_back_pressed(self):
print('on_back_pressed')
if self.webview.canGoBack():
self.webview.goBack()
else:
self.dismiss()
return True
class BrowserApp(App):
def build(self):
w = Builder.load_string('''
#:import Factory kivy.factory.Factory
FloatLayout:
Button:
text: 'Open Webview'
size_hint: (0.4, 0.2)
pos_hint: {'x':.3, 'y':0.1}
on_press: Factory.WebViewPopup().open()
''')
return w
if __name__ == '__main__':
BrowserApp().run() How to fix: move webview settings to a java file (create MyWebView.java, containing all the settings needed). Case 2 import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.utils import platform
from kivy.uix.widget import Widget
from kivy.uix.modalview import ModalView
from kivy.clock import Clock
from jnius import autoclass
from android.runnable import run_on_ui_thread
WebView = autoclass('android.webkit.WebView')
activity = autoclass('org.kivy.android.PythonActivity').mActivity
from jnius import autoclass, PythonJavaClass, java_method, cast
KeyEvent = autoclass('android.view.KeyEvent')
LayoutParams = autoclass('android.view.ViewGroup$LayoutParams')
ViewGroup = autoclass('android.view.ViewGroup')
from kivy.core.window import Window
class KeyListener(PythonJavaClass):
__javacontext__ = 'app'
__javainterfaces__ = ['android/view/View$OnKeyListener']
def __init__(self, listener):
super().__init__()
self.listener = listener
@java_method('(Landroid/view/View;ILandroid/view/KeyEvent;)Z')
def onKey(self, v, key_code, event):
if event.getAction() == KeyEvent.ACTION_DOWN and key_code == KeyEvent.KEYCODE_BACK:
return self.listener()
class WebViewPopup(ModalView):
def __init__(self, *, url=None, data=None, **kwargs):
super().__init__(**kwargs)
self.auto_dismiss = False
@run_on_ui_thread
def create_webview(self):
webview = WebView(activity)
activity.addContentView(webview, LayoutParams(-1,-1))
webview.setOnKeyListener(KeyListener(self.on_back_pressed))
self.webview = webview
@run_on_ui_thread
def on_open(self):
self.create_webview()
self.webview.loadUrl('https://duckduckgo.com/')
@run_on_ui_thread
def on_dismiss(self):
parent = cast(ViewGroup, self.webview.getParent())
if parent is not None: parent.removeView(self.webview)
self.webview = None
@run_on_ui_thread
def on_back_pressed(self):
if self.webview.canGoBack():
self.webview.goBack()
else:
self.dismiss()
return True
class BrowserApp(App):
def build(self):
self.bind(on_start=self.post_build_init)
w = Builder.load_string('''
#:import Factory kivy.factory.Factory
FloatLayout:
TextInput:
size_hint: (0.4, 0.2)
pos_hint: {'x':.3, 'y':0.5}
Button:
text: 'Open widget'
size_hint: (0.4, 0.2)
pos_hint: {'x':.3, 'y':0.1}
on_press: Factory.WebViewPopup().open()
''')
return w
def post_build_init(self, ev):
Clock.schedule_interval(lambda dt: print('Window.height =', Window.height), 1.0)
if __name__ == '__main__':
BrowserApp().run() How to fix: remove TextInput widget or post_build_init method. Case 3 import kivy
from kivy.app import App
from kivy.lang import Builder
from kivy.utils import platform
from kivy.uix.widget import Widget
from kivy.uix.modalview import ModalView
from kivy.clock import Clock
from jnius import autoclass
from android.runnable import run_on_ui_thread
WebView = autoclass('android.webkit.WebView')
WebViewClient = autoclass('android.webkit.WebViewClient')
activity = autoclass('org.kivy.android.PythonActivity').mActivity
from jnius import autoclass, PythonJavaClass, java_method, cast
KeyEvent = autoclass('android.view.KeyEvent')
LayoutParams = autoclass('android.view.ViewGroup$LayoutParams')
ViewGroup = autoclass('android.view.ViewGroup')
from kivy.core.window import Window
class KeyListener(PythonJavaClass):
__javacontext__ = 'app'
__javainterfaces__ = ['android/view/View$OnKeyListener']
def __init__(self, listener):
super().__init__()
self.listener = listener
@java_method('(Landroid/view/View;ILandroid/view/KeyEvent;)Z')
def onKey(self, v, key_code, event):
if event.getAction() == KeyEvent.ACTION_DOWN and key_code == KeyEvent.KEYCODE_BACK:
return self.listener()
class WebViewPopup(ModalView):
def __init__(self, *, url=None, data=None, **kwargs):
super().__init__(**kwargs)
self.auto_dismiss = False
@run_on_ui_thread
def create_webview(self):
webview = WebView(activity)
wvc = WebViewClient()
webview.setWebViewClient(wvc)
activity.addContentView(webview, LayoutParams(-1,-1))
webview.setOnKeyListener(KeyListener(self.on_back_pressed))
self.webview = webview
@run_on_ui_thread
def on_open(self):
self.create_webview()
self.webview.loadUrl('https://duckduckgo.com/')
@run_on_ui_thread
def on_dismiss(self):
self.webview.clearHistory()
self.webview.clearCache(True)
self.webview.clearFormData()
self.webview.freeMemory()
parent = cast(ViewGroup, self.webview.getParent())
if parent is not None: parent.removeView(self.webview)
self.webview = None
@run_on_ui_thread
def on_back_pressed(self):
if self.webview.canGoBack():
self.webview.goBack()
else:
self.dismiss()
return True
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.button import Button
class Btn(Button):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(on_press = lambda instance: WebViewPopup().open())
class BrowserApp(App):
def build(self):
w = Builder.load_string('''
#:import Factory kivy.factory.Factory
#:import Window kivy.core.window.Window
ScreenManager:
Screen1
Screen2
<Screen2@Screen>:
name: 'screen2'
RecycleView:
id: rv
size_hint_y: 1.0
viewclass: 'Btn'
data: [{'text': str(x)} for x in range(10)]
RecycleBoxLayout:
orientation: 'vertical'
size_hint_y: None
default_size_hint: (1, None)
height: self.minimum_height
default_size: None, dp(Window.height/15)
spacing: 5
<Screen1@Screen>:
name: 'screen1'
FloatLayout:
Button:
text: 'To screen 2'
size_hint: (0.4, 0.2)
pos_hint: {'x':.3, 'y':0.1}
on_press: app.root.current = 'screen2'; app.root.get_screen('screen2').ids['rv'].data = [{'text': str(x)} for x in range(100)]
''')
return w
if __name__ == '__main__':
BrowserApp().run() How to fix: remove Window.height form RecycleBoxLayout.default_size (e.g. use fixed size instead). Maybe the problem is the same as in that issue. |
Anyway I can't be sure that the bug won't appear in some other situations, so an important question remains: what is the proper way of catching such an error to prevent app from crashing (kivy.base.ExceptionHandler doesn't do the job). |
Here is my Kivy Webview that exits with a back button or gesture, as far as I know it works just fine (but I don't know everything). There are differences: It does not have the address bar. I notice that my Webview sits inside a LinearLayout, as this was important for some reason I don't remember. Two of your examples do not have WebviewClient, which seems like would be an issue. There are probably more differences. The symptoms you describe still look like app memory issues. The most likely cause is that there are two garbage collectors working on the same heap, and they don't know each other's boundaries. We have to pay attention to the lifetime of Java instances held in Python variables. Another possible cause is misuse of Java classes. If Python garbage collects an active Java object the resulting crashes are in Java, so Python exception handing is not going to catch the issue. In the logcat the issue will not show up in a Python Traceback for the same reason. The logcat will show some Java info but that is usually not particularly helpful. These can be hard issues to find in an app, but you already know that. ☹ |
Thanks for the code! But Case 3 still has a bug (at least for me, on Android 10) (and that one too), and I could break your app by changing build method in the following way: def build(self):
self._create_local_file()
self.browser = None
w = Builder.load_string('''
#:import Factory kivy.factory.Factory
#:import Window kivy.core.window.Window
ScreenManager:
Screen1
Screen2
<Screen2@Screen>:
name: 'screen2'
RecycleView:
id: rv
size_hint_y: 1.0
viewclass: 'Btn'
data: [{'text': str(x)} for x in range(10)]
RecycleBoxLayout:
orientation: 'vertical'
size_hint_y: None
default_size_hint: (1, None)
height: self.minimum_height
default_size: None, dp(Window.height/15)
spacing: 5
<Screen1@Screen>:
name: 'screen1'
FloatLayout:
Button:
text: 'To screen 2'
size_hint: (0.4, 0.2)
pos_hint: {'x':.3, 'y':0.1}
on_press: app.root.current = 'screen2'; app.root.get_screen('screen2').ids['rv'].data = [{'text': str(x)} for x in range(100)]
''')
return w sure, one also needs to add from kivy.lang import Builder
from kivy.uix.button import Button
class Btn(Button):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(on_press = lambda instance: BrowserApp.get_running_app().view_google(None)) |
Unfortunately seems that you're right about catching the error. But it would be great to at least print more info in jnius_proxy.pxi (in addition to traceback.print_exc()). But I can't find a way to modify this file. |
Thanks for the feedback, extra tests are always good. In retrospect there is one line in my code that might (I really don't know) be dangerous
could you please change this to (the
Thank you for your time.
This is further inside the system than I understand. My current rule of thumb is make every Python variable (that references Java managed memory) a class variable. |
Strange... I've run the apk (including your suggestion about self.wvc) in the Android Studio for Android 11 (Google Play), x86_64, api=30, and replicated the bug. Maybe the matter is somewhere in the buildozer.spec. Please find my below.
|
Using my code (as published) with your kv added: I tried running on an x86_64 A11 emulator, build for x86_64, but it crashed on loading screen. Not what you are seeing, I assume this something here or about the _64 emulator - always seems strange that the 32 bit emulator is Google's recommended emulator. I tried running on an x86 A11 emulator, build for x86. Works fine. |
I'll try to install Kivy and build an apk on Windows 2-3 weeks later. Curious enough whether I'm able to reproduce the issue or not. |
Anyway I found a simple way to overcome the bug - just close webview when the app is hiding and show it again when the app is restoring. So if I change pause and resume methods in your webview.py to the following ones, everything works fine: def pause(self):
if self.webview:
parent = cast(ViewGroup, self.layout.getParent())
if parent is not None: parent.removeView(self.layout)
def resume(self):
if self.webview:
PythonActivity.mActivity.addContentView(self.layout, LayoutParams(-1,-1)) |
Well found. I'll play with it some time soon. |
Tried building using KivyCompleteVM (without changing buildozer.spec there), but this modification of your code still crashes. |
I'm trying to implement Android webview with the address bar and Android back button support.
When I open webview, focus on the address bar, then press Android back button, everything works fine (webview closes). But when I open webview for the next time and peform the same actions, the app crashes.
If one waits several seconds before the last Android back button press, the app (usually) shows the following traceback:
Pyjnius version: 1.2.1.
The code is below:
The text was updated successfully, but these errors were encountered: