codenameone / CodenameOne

Cross-platform framework for building truly native mobile apps with Java or Kotlin. Write Once Run Anywhere support for iOS, Android, Desktop & Web.
https://www.codenameone.com/
Other
1.71k stars 405 forks source link

[BUG] AutoCompleteTextField crash with an indexoutofbounds exception #2770

Open ramsestom opened 5 years ago

ramsestom commented 5 years ago

In some edge cases AutoCompleteTextField was crashing and returning an indexoutofbounds exception. Fixed it in my fork by replacing line 523 if(((List)popup.getComponentAt(0)).getModel().getSize() == 0){ by if( popup.getComponentCount()==0 || ((List)popup.getComponentAt(0)).getModel().getSize() == 0){ (didn't bother to make a PR as it is a simple one line fix)

ramsestom commented 5 years ago

EDIT: actually this fix prevent the AutoCompleteTextField to crash but do not fix the underlying bug. It appears that the AutoCompleteTextField do not always show the suggestions popup and the really weird thing is that executing the exact same code multiple times in the simulator sometimes work and sometimes dont... My code use an AutoCompleteTextField with a filter defined like this:

 final DefaultListModel<String> options = new DefaultListModel<>();
AutoCompleteTextField gui_markTF = new AutoCompleteTextField(options) {
            @Override
            protected boolean filter(String text) {
                if(text.length() == 0) {
                    return false;
                }
                String[] l = getBrands(text); 
                if(l == null || l.length == 0) {
                    return false;
                }

                options.removeAll();
                for(String s : l) {
                    options.addItem(s);
                }
                return true;
            }
        };

where getBrands(text) is a simple function that always return the same String array for now (I didn't implement the server requesting part yet).

public static String[] getBrands(String text) { 
        return new String[] {"Ba", "Be", "Bi", "Bo", "Bu", "Machin", "Truc", "Bidule"};
}

and when I click on the AutoCompleteTextField, the suggestion popup sometimes shows and sometimes dont (previously to my fix it was sometimes crashing and sometimes not). So it means that popup.getComponentCount() is sometimes equals to 0 and sometimes not when reaching line 523 of the AutoCompleteTextField code. That fact that the same code produce different results when run multiple times makes me think that there is some kind of race condition bug in the current AutoCompleteTextField implementation...

ramsestom commented 5 years ago

I investigated a bit more this issue and was able to track it down to the fact that the Display.getInstance().callSerially call inside the initComponent() function of AutoCompleteTextField is not always called the first time the Form that contains this AutoCompleteTextField is displayed. Actually I modified the initComponent() function like this:

  protected void initComponent() {
        super.initComponent();
        System.out.println("init AutoCompleteTextField");
        getComponentForm().addPointerPressedListener(pressListener);
        getComponentForm().addPointerReleasedListener(listener);
        Display.getInstance().callSerially(new Runnable() {

            @Override
            public void run() {
                System.out.println("call serially addPopup");
                addPopup();
            }
        });
    }

and, created a test app with a Form "FA" containing some button allowing to navigate to (=show) a Form FB containing an AutoCompleteTextField and a Toolbar back command allowing to go back to FA. When I navigate from FA to FB, most of the times, everything is OK and the simulator console immediatly prints "init AutoCompleteTextField" followed by "call serially addPopup". But, in some cases, this is not the case. Sometimes the console actually only prints "init AutoCompleteTextField" when FB is shown and this is only when I go back to FA that "call serially addPopup" is finally displayed (so when the AutoCompleteTextField is no longer visible). No matter how long I stay in FB in that case or even if I try to interact with others components in it (I put some TextComponent in it as the more "complex" the Form FB containing the AutoCompleteTextField is, the most probable it seems to see the bug occur), this is only when I quit FB for FA that "call serially addPopup" would be displayed in that case. I even modified the deinitialize() function of AutoCompleteTextField the same way as its initComponent() function and when everything want fine the console output is:

//navigate from FA to FB
init AutoCompleteTextField
call serially addPopup
//navigate from FB to FA
deinit AutoCompleteTextField
call serially removePopup

whereas when the bug occurs I have:

//navigate from FA to FB
init AutoCompleteTextField
//navigate from FB to FA
deinit AutoCompleteTextField
call serially removePopup
init AutoCompleteTextField
call serially addPopup
call serially addPopup

why on earth "call serially addPopup" is only called when the AutoCompleteTextField is no longer visible and why a second initComponent() call seems to be made too in that case is a complete mystery.

I tried to create a simple test case to help you reproduce the issue but unfortunatelly didn't succeed yet as not only the bug not always occurs but also it seems that FB has to be "complex" enaugh to have a high probability for this bug to occur (I tried with FB containing only an AutoCompleteTextField component but was only able to see the bug ~1/50 or 1/100 times in that case whereas when I use a "real life" FB that, additionaly to the AutoCompleteTextField, also contains multiple TextComponent with bindings to Property objects, the probability for the bug to occur is much higher (~1/5 or 1/10) I also noticed that the bug seems to be time dependant. Actually, if I stay long enaugh on Form FA before navigating to Form FB the bug wont appear whereas if I navigate quickly (1s - 5s) to FB just after FA is shown, I have a chance to see the bug... Also, when the bug appear, this is only the first time I navigate from FA to FB. Actually if I navigate back to FA and then create a new Form FB and navigate to it, the bug would not appear on that new FB (or at least I never saw it on that case)

So it seems that this bug is actually not directly related to the AutoCompleteTextFieldcode but is linked to a bug in the EDT calls flow that might not work as expected in some edge cases (which makes it really difficult to track down but is also a bit more annoying as this means that this might potentially affect more components using Display.getInstance().callSerially and not only AutoCompleteTextField...)

shannah commented 5 years ago

Can you post a small, self-contained test case that can reproduce the problem?

ramsestom commented 5 years ago

I tried but unfortunately, the hazardeous nature of this bug makes it pretty much impossible to reproduce with a test-case. I had a simple test case where I was able to see the bug twice after multiple (~30-40) tries but I didn't succeed to reproduce the bug with it since. On my real app, which is way more complicated, the bug is much more frequent (I can reproduce it about 1/3 times if I follow a specific pattern (quickly navigate between Forms) ) but I can't easilly extract a self-contained test case as these Forms depends on many other parts of my app (they use property bindings to data retrieved from a server and an app SQLite local database too)) Anyway, the impact of this bug is quite minimal on my app now as, with my fix, when it happens, the app is not crashing anymore and the AutoCompleteTextField suggestion popup is just not displayed (I can live with that especially since it should be quite rare for my user to quickly navigate from Form to Form). I already spent hours trying to track this bug and can't really offer to spend more so I am afraid I wouldn't be able to post a self-contained test case that would alows you to reproduce this bug with a high probability. I am aware the cause of this bug would probably be really hard to fix for now as it is quite difficult to track it down up to the root of the issue. I just posted a detailled description of why and when this bug appears in my case in case you guys have an idea and if you see any other strange bug that might be related to Display.getInstance().callSerially it may give you a hint and maybe a chance to finally fix it in the future.

shannah commented 5 years ago

A simple test case that reproduces the bug 1 time in 40 is sufficient for me to get started on it.

ramsestom commented 5 years ago

Made a simple test case: https://github.com/ramsestom/CN1-AutoCompleteTextField-Bug

you have to be quite lucky to see the bug (I tried ~30times again to finally see it) with this simple version. I found that with this version you have to click at the exact same time the message is about to fold (so at 1s) to increase your chance to see the bug (in my real app, this is not the case and even if I click when the tip message is opening or folding the bug can appear but with this simple test case I only could see the bug when I clicked a button just after the tip message opened and before it start folding. So the bug has probably something to do with a race condition on animations). Without the fix into AutoCompleteTextField, your app will display an error message anf freeze when the bug occurs and you will see something like this in the console:

[EDT] 0:0:6,10 - Exception: java.lang.IndexOutOfBoundsException - Index: 0, Size: 0
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    at java.util.ArrayList.rangeCheck(ArrayList.java:657)
    at java.util.ArrayList.get(ArrayList.java:433)
    at com.codename1.ui.Container.getComponentAt(Container.java:1584)
    at com.codename1.ui.AutoCompleteTextField$FormPointerListener.actionPerformed(AutoCompleteTextField.java:472)
    at com.codename1.ui.util.EventDispatcher.fireActionEvent(EventDispatcher.java:349)
    at com.codename1.ui.Form.pointerReleased(Form.java:2922)
    at com.codename1.ui.Component.pointerReleased(Component.java:4100)
    at com.codename1.ui.Display.handleEvent(Display.java:2061)
    at com.codename1.ui.Display.edtLoopImpl(Display.java:1043)
    at com.codename1.ui.Display.invokeAndBlock(Display.java:1186)
    at com.codename1.ui.Display.invokeAndBlock(Display.java:1223)
    at com.codename1.ui.AnimationManager.addAnimationAndBlock(AnimationManager.java:105)
    at com.codename1.ui.ComponentSelector.animateUnlayoutAndWait(ComponentSelector.java:3584)
    at com.codename1.ui.ComponentSelector.slideUpAndWait(ComponentSelector.java:1036)
    at com.codename1.components.ToastBar.setVisible(ToastBar.java:810)
    at com.codename1.components.ToastBar.updateStatus(ToastBar.java:521)
    at com.codename1.components.ToastBar.removeStatus(ToastBar.java:754)
    at com.codename1.components.ToastBar.access$700(ToastBar.java:84)
    at com.codename1.components.ToastBar$Status.clear(ToastBar.java:390)
    at com.codename1.components.ToastBar$Status$1$1.run(ToastBar.java:290)
    at com.codename1.ui.Display.processSerialCalls(Display.java:1129)
    at com.codename1.ui.Display.edtLoopImpl(Display.java:1073)
    at com.codename1.ui.Display.mainEDTLoop(Display.java:961)
    at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:120)
    at com.codename1.impl.CodenameOneThread.run(CodenameOneThread.java:176)
[EDT] 0:0:6,14 - Exception: java.lang.IndexOutOfBoundsException - Index: 0, Size: 0
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    at java.util.ArrayList.rangeCheck(ArrayList.java:657)
    at java.util.ArrayList.get(ArrayList.java:433)
    at com.codename1.ui.Container.getComponentAt(Container.java:1584)
    at com.codename1.ui.AutoCompleteTextField$FormPointerListener.actionPerformed(AutoCompleteTextField.java:472)
    at com.codename1.ui.util.EventDispatcher.fireActionEvent(EventDispatcher.java:349)
    at com.codename1.ui.Form.pointerReleased(Form.java:2922)
    at com.codename1.ui.Component.pointerReleased(Component.java:4100)
    at com.codename1.ui.Display.handleEvent(Display.java:2061)
    at com.codename1.ui.Display.edtLoopImpl(Display.java:1043)
    at com.codename1.ui.Display.invokeAndBlock(Display.java:1186)
    at com.codename1.ui.Display.invokeAndBlock(Display.java:1223)
    at com.codename1.ui.AnimationManager.addAnimationAndBlock(AnimationManager.java:105)
    at com.codename1.ui.ComponentSelector.animateUnlayoutAndWait(ComponentSelector.java:3584)
    at com.codename1.ui.ComponentSelector.slideUpAndWait(ComponentSelector.java:1036)
    at com.codename1.components.ToastBar.setVisible(ToastBar.java:810)
    at com.codename1.components.ToastBar.updateStatus(ToastBar.java:521)
    at com.codename1.components.ToastBar.removeStatus(ToastBar.java:754)
    at com.codename1.components.ToastBar.access$700(ToastBar.java:84)
    at com.codename1.components.ToastBar$Status.clear(ToastBar.java:390)
    at com.codename1.components.ToastBar$Status$1$1.run(ToastBar.java:290)
    at com.codename1.ui.Display.processSerialCalls(Display.java:1129)
    at com.codename1.ui.Display.edtLoopImpl(Display.java:1073)
    at com.codename1.ui.Display.mainEDTLoop(Display.java:961)
    at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:120)
    at com.codename1.impl.CodenameOneThread.run(CodenameOneThread.java:176)
[EDT] 0:0:6,301 - Exception: java.lang.NullPointerException - null
java.lang.NullPointerException
    at com.codename1.ui.Container.removeComponentImplNoAnimationSafety(Container.java:1082)
    at com.codename1.ui.Container.removeComponentImpl(Container.java:1072)
    at com.codename1.ui.Container.removeComponent(Container.java:1027)
    at com.codename1.ui.AutoCompleteTextField.removePopup(AutoCompleteTextField.java:286)
    at com.codename1.ui.AutoCompleteTextField.access$200(AutoCompleteTextField.java:55)
    at com.codename1.ui.AutoCompleteTextField$3.run(AutoCompleteTextField.java:141)
    at com.codename1.ui.Display.processSerialCalls(Display.java:1129)
    at com.codename1.ui.Display.edtLoopImpl(Display.java:1073)
    at com.codename1.ui.Display.invokeAndBlock(Display.java:1186)
    at com.codename1.ui.Display.invokeAndBlock(Display.java:1223)
    at com.codename1.ui.Form.showModal(Form.java:2030)
    at com.codename1.ui.Dialog.showModal(Dialog.java:1091)
    at com.codename1.ui.Dialog.show(Dialog.java:553)
    at com.codename1.ui.Dialog.showPackedImpl(Dialog.java:1395)
    at com.codename1.ui.Dialog.showPacked(Dialog.java:1303)
    at com.codename1.ui.Dialog.showImpl(Dialog.java:1047)
    at com.codename1.ui.Dialog.show(Dialog.java:1025)
    at com.codename1.ui.Dialog.show(Dialog.java:981)
    at com.codename1.ui.Dialog.show(Dialog.java:764)
    at com.codename1.ui.Dialog.show(Dialog.java:717)
    at com.codename1.ui.Dialog.show(Dialog.java:682)
    at com.codename1.ui.Dialog.show(Dialog.java:623)
    at com.codename1.ui.Dialog.show(Dialog.java:778)
    at com.codename1.ui.Display.mainEDTLoop(Display.java:974)
    at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:120)
    at com.codename1.impl.CodenameOneThread.run(CodenameOneThread.java:176)
java.lang.NullPointerException
    at com.codename1.ui.Container.removeComponentImplNoAnimationSafety(Container.java:1082)
    at com.codename1.ui.Container.removeComponentImpl(Container.java:1072)
    at com.codename1.ui.Container.removeComponent(Container.java:1027)
    at com.codename1.ui.AutoCompleteTextField.removePopup(AutoCompleteTextField.java:286)
    at com.codename1.ui.AutoCompleteTextField.access$200(AutoCompleteTextField.java:55)
    at com.codename1.ui.AutoCompleteTextField$3.run(AutoCompleteTextField.java:141)
    at com.codename1.ui.Display.processSerialCalls(Display.java:1129)
    at com.codename1.ui.Display.edtLoopImpl(Display.java:1073)
    at com.codename1.ui.Display.invokeAndBlock(Display.java:1186)
    at com.codename1.ui.Display.invokeAndBlock(Display.java:1223)
    at com.codename1.ui.Form.showModal(Form.java:2030)
    at com.codename1.ui.Dialog.showModal(Dialog.java:1091)
    at com.codename1.ui.Dialog.show(Dialog.java:553)
    at com.codename1.ui.Dialog.showPackedImpl(Dialog.java:1395)
    at com.codename1.ui.Dialog.showPacked(Dialog.java:1303)
    at com.codename1.ui.Dialog.showImpl(Dialog.java:1047)
    at com.codename1.ui.Dialog.show(Dialog.java:1025)
    at com.codename1.ui.Dialog.show(Dialog.java:981)
    at com.codename1.ui.Dialog.show(Dialog.java:764)
    at com.codename1.ui.Dialog.show(Dialog.java:717)
    at com.codename1.ui.Dialog.show(Dialog.java:682)
    at com.codename1.ui.Dialog.show(Dialog.java:623)
    at com.codename1.ui.Dialog.show(Dialog.java:778)
    at com.codename1.ui.Display.mainEDTLoop(Display.java:974)
    at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:120)
    at com.codename1.impl.CodenameOneThread.run(CodenameOneThread.java:176)

with the fix, the popup would simply not display when the bug occurs

ramsestom commented 5 years ago

Made you a small video to show you the exact moment you have to click a button to have a chance to see the bug: https://streamable.com/i9e85 knowing this, it is a lot easier to catch the bug (only had to retry 3 times to see it appear) even if you still need a bit of luck ;)