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

Android webview with address bar "no attribute 'invoke'" error #575

Open
Alspb opened this issue Nov 17, 2020 · 18 comments
Open

Android webview with address bar "no attribute 'invoke'" error #575

Alspb opened this issue Nov 17, 2020 · 18 comments

Comments

@Alspb
Copy link

Alspb commented Nov 17, 2020

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:

File "jnius/jnius_proxy.pxi", line 156, in jnius.jnius.invoke0
File "jnius/jnius_proxy.pxi", line 124, in jnius.jnius.py_invoke0
AttributeError: 'list' object has no attribute 'invoke'

Pyjnius version: 1.2.1.

The code is below:

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.core.window import Window
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')
    EditText = autoclass('android.widget.EditText')
    InputType = autoclass('android.text.InputType')
    ViewGroup = autoclass('android.view.ViewGroup')

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

        address_bar = EditText(activity)
        address_bar.setInputType(InputType.TYPE_TEXT_VARIATION_URI)

        layout = LinearLayout(activity)
        layout.setOrientation(LinearLayout.VERTICAL)
        layout.addView(address_bar, Window.width, Window.height*0.1)
        layout.addView(webview, Window.width, Window.height*0.9)
        
        activity.addContentView(layout, LayoutParams(-1,-1))

        webview.setOnKeyListener(KeyListener(self.on_back_pressed))
        address_bar.setOnKeyListener(KeyListener(self.on_back_pressed))

        self.address_bar = address_bar
        self.webview = webview
        self.layout = layout

    @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()

        parent = cast(ViewGroup, self.layout.getParent())
        if parent is not None: parent.removeView(self.layout)
        self.layout = 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 unfocus_address_bar(self):       
        self.address_bar.clearFocus()
        return True

    @run_on_ui_thread
    def go_forward(self):
        if self.webview.canGoForward():
            self.webview.goForward()

    @run_on_ui_thread
    def refresh(self):
        self.webview.reload()

    @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'
        AndroidWidgetHolder:
            id: webview_holder
            size_hint: (1, 1)
'''

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()
@RobertFlatt
Copy link

RobertFlatt commented Nov 29, 2020

I thought this was such an elegant solution that I tried it.
I could not replicate the issue, it works really well, the behavior I see is repeatable.
No message from jnius and no invoke related error in the logcat.

I used buildozer (1.2.0) defaults, except android.permissions = INTERNET
Buildozer used pyjnius 1.2.1

I tested on Android 11 on Pixel.
Android 11 has no back button, back is implemented as a swipe from either screen edge.
This worked as expected, back though the web pages till finally back to Kivy button (multiple times).

Removing the address_bar code, and changing the webview height, and it still works perfectly.
And the on_back_pressed() events never came from hook_keyboard()

@Alspb
Copy link
Author

Alspb commented Dec 4, 2020

Thanks a lot for testing.
Unfortunatelly on my Android 10 phone the app crashes in the described scenario. So I tried to deeply investigate the issue and checked the apk in Android Studio Emulator (phone model - Pixel 3a, the apk is built with android.arch = x86 in buildozer.spec). The results are pretty strange:

  • Android 9.0 (Google Play) - works
  • Android 10.0 (Google Play) - fails
  • Android 11.0 (Google Play) - works
  • Android 9.0 (Google APIs) - works
  • Android 10.0 (Google APIs) - works
  • Android 11.0 (Google APIs) - works
  • Android 9.0 - fails
  • Android 10.0 - fails

@RobertFlatt
Copy link

That looks like a memory issue. Just a guess of course.
The app is pretty good at cleaning up after itself, perhaps too good?

If it were me I'd try commenting the memory free code (one step at a time), in
AndroidWidgetHolder().on_view()
WebViewPopup().on_dismiss()

Perhaps I suggest this because I know I don't know when exactly when Java code is free'd by Java.
Anyway, its a suggestion, not pretending to be an answer. Please let me know what happens.

@RobertFlatt
Copy link

I removed the address bar,
Built for x86, api=27
Tested (open wb, visit site, back, back, repeat sequence) with Pixel3a emulators 26,27,28,29,30 - no issue seen.
Might give you a clue where to look.

@Alspb
Copy link
Author

Alspb commented Dec 16, 2020

Strangely enough, but I overcame the issue by just clearing ".buildozer" folder!
Unfortunately I faced the same error in my webview with Kivy button(s) code (see below).

Steps to reproduce the error:

  1. Open webview.
  2. Press "Images" tab on Google webpage.
  3. Press "Copy" button.
  4. Press Android back button.

After that the app crashes. Sometimes (but not always) it shows the same error as above:

File "jnius/jnius_proxy.pxi", line 156, in jnius.jnius.invoke0
File "jnius/jnius_proxy.pxi", line 124, in jnius.jnius.py_invoke0
AttributeError: 'list' object has no attribute 'invoke'

Tested on Android Emulator (Pixel 3a) for Android 9-11 (with Google Play) - the app crahes.
The important difference from the bug above is that it exists on Android 11 too.
Steps 2 and 3 could also be swapped around. In this case the app still crashes, but the error is usually not shown.

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

@Alspb
Copy link
Author

Alspb commented Dec 16, 2020

I removed the address bar,
Built for x86, api=27
Tested (open wb, visit site, back, back, repeat sequence) with Pixel3a emulators 26,27,28,29,30 - no issue seen.
Might give you a clue where to look.

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.

That looks like a memory issue. Just a guess of course.
The app is pretty good at cleaning up after itself, perhaps too good?

If it were me I'd try commenting the memory free code (one step at a time), in
AndroidWidgetHolder().on_view()
WebViewPopup().on_dismiss()

Perhaps I suggest this because I know I don't know when exactly when Java code is free'd by Java.
Anyway, its a suggestion, not pretending to be an answer. Please let me know what happens.

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.

@Alspb
Copy link
Author

Alspb commented Apr 25, 2021

Finally got the simple WebView implementations containing the bug above (at least on Android 10).
Steps to reproduce:

  1. Open webview
  2. Hide an app (press Android Home button), then restore it
  3. Focus on the search bar.
  4. Hide keyboard appeared, then press Back button.
    Below each of the cases please find a fix. Don't have any idea why they work...

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.

@Alspb
Copy link
Author

Alspb commented Apr 25, 2021

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

@RobertFlatt
Copy link

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).
https://github.com/RobertFlatt/Android-for-Python/tree/main/webview

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. ☹

@Alspb
Copy link
Author

Alspb commented Apr 26, 2021

Thanks for the code!
You're right, using LinearLayout fixes Cases 1 and 2!
WebviewClient doesn't influence anything, and I removed it to simplify the examples (forgot to remove it from the Case 3).

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

@Alspb
Copy link
Author

Alspb commented Apr 26, 2021

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.

@RobertFlatt
Copy link

I could break your app by changing build method in the following way:

Thanks for the feedback, extra tests are always good.
I inserted the changes, built on api=30, arm64-v8a; and ran on Android 11.
But I could not replicate, I tried several buttons up to 99, and browsing away from the default page.
Is there any specific UI event sequence to replicate?

In retrospect there is one line in my code that might (I really don't know) be dangerous

        webview.setWebViewClient(WebViewClient())

could you please change this to (the self. is important), and try again.

       self.wvc = WebViewClient()
       webview.setWebViewClient(self.wvc)

Thank you for your time.

info in jnius_proxy.pxi (in addition to traceback.print_exc()). But I can't find a way to modify this file.

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.
This might be overkill, but it seems to work.
In the case above, there might be an anonymous Python variable containing the parameter result- hence the test.

@Alspb
Copy link
Author

Alspb commented Apr 27, 2021

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.
Also attach the gif with what I'm doing to break the app (sorry, emulated phone is a bit slow).
Once again, this particular Case could be easily solved by removing "Window.height", but I can't be sure that the bug won't appear in the more sophisticated cases. But maybe this particular bug is connected to this issue with Window.height.
webview_crash

[app]

# (str) Title of your application
title = WebView_test

# (str) Package name
package.name = webview

# (str) Package domain (needed for android/ios packaging)
#???
package.domain = abc.cde

# (str) Source code where the main.py live
source.dir = .

# (list) Source files to include (let empty to include all the files)
#py,png,jpg,kv,atlas
source.include_exts = py,kv 

# (list) List of inclusions using pattern matching
#source.include_patterns = assets/*,images/*.png

# (list) Source files to exclude (let empty to not exclude anything).
#source.exclude_exts = db

# (list) List of directory to exclude (let empty to not exclude anything)
source.exclude_dirs = tests, backup, temp, bin, docs, kivy_logs

# (list) List of exclusions using pattern matching
#source.exclude_patterns = license,images/*/*.jpg

# (str) Application versioning (method 1)
version = 0.1

# (str) Application versioning (method 2)
#version.regex = __version__ = ['"](.*)['"]
#version.filename = %(source.dir)s/main.py

# (list) Application requirements (do not include standard library!), comma separated
# Версию python3 можно задавать, если buildozer стал поддерживать более новую, но для гарантии стабильной работы build нужно делать на старой. А так по умолчанию используется наиболее актуальная из поддерживаемых версий
#requirements = python3==3.8.5,hostpython3==3.8.5,kivy==2.0.0rc3,lxml,pygments
#requirements = python3,kivy==2.0.0rc3,lxml,pygments
requirements = python3,kivy==2.0.0
#todel!
#requirements = python3,kivy==master,lxml,pygments

# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy

# (list) Garden requirements
#garden_requirements =

# (str) Presplash of the application
#presplash.filename = %(source.dir)s/data/presplash.png

# (str) Icon of the application
#icon.filename = %(source.dir)s/data/icon.png

# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = portrait

# (list) List of service to declare
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY

#
# OSX Specific
#

#
# author = © Copyright Info

# change the major version of python used by the app
#osx.python_version = 3

# Kivy version to use
#osx.kivy_version = 1.11.1

#
# Android specific
#

# (bool) Indicate if the application should be fullscreen or not
fullscreen = 0

# (string) Presplash background color (for new android toolchain)
# Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
# olive, purple, silver, teal.
#android.presplash_color = #FFFFFF

# (list) Permissions
# For base version
android.permissions = INTERNET
# For offline version
#android.permissions = WRITE_EXTERNAL_STORAGE,READ_EXTERNAL_STORAGE

# (int) Target Android API, should be as high as possible.
#android.api = 28

# (int) Minimum API your APK will support.
#android.minapi = 21

# (int) Android SDK version to use
#android.sdk = 20

# (str) Android NDK version to use
#android.ndk = 19b

# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi.
#android.ndk_api = 21

# (bool) Use --private data storage (True) or --dir public storage (False)
#android.private_storage = True

# (str) Android NDK directory (if empty, it will be automatically downloaded.)
#android.ndk_path =

# (str) Android SDK directory (if empty, it will be automatically downloaded.)
#android.sdk_path =

# (str) ANT directory (if empty, it will be automatically downloaded.)
#android.ant_path =

# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
# android.skip_update = False

# (bool) If True, then automatically accept SDK license
# agreements. This is intended for automation only. If set to False,
# the default, you will be shown the license when first running
# buildozer.
# android.accept_sdk_license = False

# (str) Android entry point, default is ok for Kivy-based app
#android.entrypoint = org.renpy.android.PythonActivity

# (list) Pattern to whitelist for the whole project
#android.whitelist =

# (str) Path to a custom whitelist file
#android.whitelist_src =

# (str) Path to a custom blacklist file
#android.blacklist_src =

# (list) List of Java .jar files to add to the libs so that pyjnius can access
# their classes. Don't add jars that you do not need, since extra jars can slow
# down the build process. Allows wildcards matching, for example:
# OUYA-ODK/libs/*.jar
#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar

# (list) List of Java files to add to the android project (can be java or a
# directory containing the files)
#android.add_src = java_classes

# (list) Android AAR archives to add (currently works only with sdl2_gradle
# bootstrap)
#android.add_aars = support-compat-28.0.0.aar

# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
#android.gradle_dependencies =

# (list) Java classes to add as activities to the manifest.
#android.add_activites = com.example.ExampleActivity

# (str) python-for-android branch to use, defaults to master
#p4a.branch = master

# (str) OUYA Console category. Should be one of GAME or APP
# If you leave this blank, OUYA support will not be enabled
#android.ouya.category = GAME

# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png

# (str) XML file to include as an intent filters in <activity> tag
#android.manifest.intent_filters =

# (str) launchMode to set for the main activity
# ??Может лучше singleTask ?
#android.manifest.launch_mode = standard

# (list) Android additional libraries to copy into libs/armeabi
#android.add_libs_armeabi = libs/android/*.so
#android.add_libs_armeabi_v7a = libs/android-v7/*.so
#android.add_libs_x86 = libs/android-x86/*.so
#android.add_libs_mips = libs/android-mips/*.so

# (bool) Indicate whether the screen should stay on
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False

# (list) Android application meta-data to set (key=value format)
#android.meta_data =

# (list) Android library project to add (will be added in the
# project.properties automatically.)
#android.library_references =

# (str) Android logcat filters to use
android.logcat_filters = *:S python:D

# (bool) Copy library instead of making a libpymodules.so
#android.copy_libs = 1

# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86
android.arch = x86

#
# Python for android (p4a) specific
#

# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
#p4a.source_dir = ./python-for-android
#p4a.source_dir = /home/al/Downloads/python-for-android

# (str) The directory in which python-for-android should look for your own build recipes (if any)
#p4a.local_recipes =

# (str) Filename to the hook for p4a
#p4a.hook =

# (str) Bootstrap to use for android builds
p4a.bootstrap = sdl2

# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
#p4a.port =


#
# iOS specific
#

# (str) Path to a custom kivy-ios folder
#ios.kivy_ios_dir = ../kivy-ios
# Alternately, specify the URL and branch of a git checkout:
ios.kivy_ios_url = https://github.com/kivy/kivy-ios
ios.kivy_ios_branch = master

# Another platform dependency: ios-deploy
# Uncomment to use a custom checkout
#ios.ios_deploy_dir = ../ios_deploy
# Or specify URL and branch
ios.ios_deploy_url = https://github.com/phonegap/ios-deploy
ios.ios_deploy_branch = 1.7.0

# (str) Name of the certificate to use for signing the debug version
# Get a list of available identities: buildozer ios list_identities
#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"

# (str) Name of the certificate to use for signing the release version
#ios.codesign.release = %(ios.codesign.debug)s


[buildozer]

# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2

# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 1

# (str) Path to build artifact storage, absolute or relative to spec file
#build_dir = ./.buildozer

# (str) Path to build output (i.e. .apk, .ipa) storage
# bin_dir = ./bin

#    -----------------------------------------------------------------------------
#    List as sections
#
#    You can define all the "list" as [section:key].
#    Each line will be considered as a option to the list.
#    Let's take [app] / source.exclude_patterns.
#    Instead of doing:
#
#[app]
#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
#    This can be translated into:
#
#[app:source.exclude_patterns]
#license
#data/audio/*.wav
#data/images/original/*
#


#    -----------------------------------------------------------------------------
#    Profiles
#
#    You can extend section / key with a profile
#    For example, you want to deploy a demo version of your application without
#    HD content. You could first change the title to add "(demo)" in the name
#    and extend the excluded directories to remove the HD content.
#
#[app@demo]
#title = My Application (demo)
#
#[app:source.exclude_patterns@demo]
#images/hd/*
#
#    Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug

@RobertFlatt
Copy link

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.

@Alspb
Copy link
Author

Alspb commented May 2, 2021

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.

@Alspb
Copy link
Author

Alspb commented May 2, 2021

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

@RobertFlatt
Copy link

Well found. I'll play with it some time soon.

@Alspb
Copy link
Author

Alspb commented May 11, 2021

Tried building using KivyCompleteVM (without changing buildozer.spec there), but this modification of your code still crashes.

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

No branches or pull requests

2 participants