ubergeek42 / weechat-android

Simple Weechat-Relay Android Client
518 stars 103 forks source link

Rare crashes in buffer list's `RecyclerView` #512

Open oakkitten opened 3 years ago

oakkitten commented 3 years ago

there are a few very rare bugs related to buffer list's RecyclerView and its item animator. these bugs has been present in the app for a long time. it is possible to reproduce them, but it takes a bit of effort, even then it's not reliable and can take tens of minutes to reproduce. i could not reproduce these bugs in isolated environment.

at this time my best guess is some obscure race condition in the library code. for the first bug to manifest, apparently you have to trigger a change animation, which is then ended by subsequent animation:

private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
    ...
    } else if (changeInfo.oldHolder == item) {
        changeInfo.oldHolder = null;
    ...

DefaultItemAnimator.runPendingAnimations() then schedules animation using the deleted view holder, which leads to a crash:

if (changesPending) {
    if (removalsPending) {
        RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
        ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());

the holder in question comes from mPendingChanges. i gather it shouldn't be there as it must've been removed by something around the call to endChangeAnimationIfNecessary. so the RecyclerView is in some bad state. i think this underlying issue causes the other crashes as well.

in order of the frequency of appearance, these are:

java.lang.NullPointerException: Attempt to read from field 'android.view.View androidx.recyclerview.widget.RecyclerView$ViewHolder.itemView' on a null object reference
    at androidx.recyclerview.widget.DefaultItemAnimator.runPendingAnimations(DefaultItemAnimator.java:157)
    at androidx.recyclerview.widget.RecyclerView$2.run(RecyclerView.java:589)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:967)
    at android.view.Choreographer.doCallbacks(Choreographer.java:791)
    at android.view.Choreographer.doFrame(Choreographer.java:722)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:952)
    at android.os.Handler.handleCallback(Handler.java:883)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loop(Looper.java:214)
    at android.app.ActivityThread.main(ActivityThread.java:7356)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)
java.lang.IllegalArgumentException: view is not a child, cannot hide android.widget.RelativeLayout{b13601 VFE...C.. ......ID 0,981-539,1066 #7f08004e app:id/bufferlist_item_container}
    at androidx.recyclerview.widget.ChildHelper.unhide(ChildHelper.java:352)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getScrapOrHiddenOrCachedHolderForPosition(RecyclerView.java:6393)
    at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5896)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5858)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5854)
    at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2230)
    at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1557)
    at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1517)
    at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:612)
    at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:3924)
    at androidx.recyclerview.widget.RecyclerView.onMeasure(RecyclerView.java:3336)
    at android.view.View.measure(View.java:25086)
    at android.widget.RelativeLayout.measureChildHorizontal(RelativeLayout.java:735)
    at android.widget.RelativeLayout.onMeasure(RelativeLayout.java:481)
    at android.view.View.measure(View.java:25086)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6872)
    at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
    at android.widget.LinearLayout.measureHorizontal(LinearLayout.java:1204)
    at android.widget.LinearLayout.onMeasure(LinearLayout.java:723)
    at android.view.View.measure(View.java:25086)
    at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6872)
    at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
    at androidx.appcompat.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:146)
    at android.view.View.measure(View.java:25086)
java.lang.RuntimeException: trying to unhide a view that was not hiddenandroid.widget.TextView{74540c2 VFED..C.. ........ 0,807-196,859}
    at androidx.recyclerview.widget.ChildHelper.unhide(ChildHelper.java:355)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getScrapOrHiddenOrCachedHolderForPosition(RecyclerView.java:6393)
    at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5896)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5858)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5854)
    at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2230)
    at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1557)
    at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1517)
    at androidx.recyclerview.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1331)
    at androidx.recyclerview.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:1075)
    at androidx.recyclerview.widget.RecyclerView.scrollStep(RecyclerView.java:1832)
    at androidx.recyclerview.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:5067)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:967)
    at android.view.Choreographer.doCallbacks(Choreographer.java:791)
    at android.view.Choreographer.doFrame(Choreographer.java:722)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:952)
    at android.os.Handler.handleCallback(Handler.java:883)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loop(Looper.java:214)
    at android.app.ActivityThread.main(ActivityThread.java:7356)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)

(ignore the fact that it's a TextView above, not bufferlist_item_container )

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 14(offset:-1).state:15 androidx.recyclerview.widget.RecyclerView{9dd3ee8 VFED.V... ......ID 0,0-539,957 #7f0800e7 app:id/recycler}, adapter:com.ubergeek42.WeechatAndroid.adapters.BufferListAdapter@c99d850, layout:com.ubergeek42.WeechatAndroid.views.FullScreenDrawerLinearLayoutManager@b49aa49, context:com.ubergeek42.WeechatAndroid.WeechatActivity@89ce4b9
    at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5923)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5858)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5854)
    at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2230)
    at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1557)
    at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1517)
    at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:612)
    at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:3875)
    at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3639)
    at androidx.recyclerview.widget.RecyclerView.consumePendingUpdateOperations(RecyclerView.java:1888)
    at androidx.recyclerview.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:5044)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:967)
    at android.view.Choreographer.doCallbacks(Choreographer.java:791)
    at android.view.Choreographer.doFrame(Choreographer.java:722)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:952)
    at android.os.Handler.handleCallback(Handler.java:883)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loop(Looper.java:214)
    at android.app.ActivityThread.main(ActivityThread.java:7356)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:491)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:940)

These can be triggered on current search & master branches by simulating rapid buffer list changes by using this and perhaps by having the following in WeechatActivity.onHotlistSelected():

@MainThread @Cat("Menu") private fun onHotlistSelected() {
    val buffer = BufferList.buffers.shuffled().firstOrNull { it.hotCount > 0 }

    if (buffer != null) {
        openBuffer(buffer.pointer)
    } else {
        Toaster.ShortToast.show(R.string.error__etc__no_hot_buffers)
    }

    val closeDelay = if (Random.nextInt(0, 2) != 0) Random.nextLong(0, 3000) else 0
    if (closeDelay != 0L) {
        Weechat.runOnMainThread({
            val pointer = pagerAdapter.currentBufferPointer
            if (pointer != 0L) closeBuffer(pointer)
        }, closeDelay)
    }
    val switchDelay = closeDelay + Random.nextLong(0, 3000)
    Weechat.runOnMainThread({ onHotlistSelected() }, switchDelay)
}
oakkitten commented 3 years ago

some thoughts & ideas:

what seems to help is; moving RecyclerView from outside the enclosing RelativeLayout..¯\_(ツ)_/¯

also, another related issue might be some stuck rows. they are usually semi transparent so must've been abandoned in the process of animation. the animation itself got canceled, but the view wasn't removed or recycled or anything and it just stays there. probably another manifestation of the underlying issue

also, the last crash was mentioned in #459

oakkitten commented 3 years ago

the issue might happen a tad more rarely now but still exists

oakkitten commented 3 years ago

apparently this issue affects buffer fragment's RecyclerView, as reported in #525:

Process: com.ubergeek42.WeechatAndroid, PID: 12400
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 5(offset:-1).state:17 com.ubergeek42.WeechatAndroid.views.AnimatedRecyclerView{e41407a V.ED.V... ......ID 0,0-1080,1722 #7f090054 app:id/chat_lines}, adapter:com.ubergeek42.WeechatAndroid.adapters.ChatLinesAdapter@bc9d02b, layout:androidx.recyclerview.widget.LinearLayoutManager@e9cec88, context:com.ubergeek42.WeechatAndroid.WeechatActivity@4f9e700
   at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:133)
   at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:9)
   at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1)
   at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:12)
   at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:102)
   at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:59)
   at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:7)
   at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:3)
   at android.view.View.layout(View.java:22466)
   ...