- ConversationGetMucOptionsRaceTest is in its own file to make the
instrumentation barriers a little easier to see and enforce
- It simulates basically the following situation:
i. On app startup, all accounts connect to all MUCs, which
eventually triggers joinMuc, which calls
Conversation.resetMucOptions.
This happens on account's individual XMPP connection Thread.
ii. At the same time, on the UI thread, we bind
ConversationsOverviewFragment, which eventually leads to
ConversationAdapter.onBindViewHolder, which calls getMucOptions.
iii. Conversation.resetMucOptions is not synchronized, so it can
run after the null-check in getMucOptions, resulting in a null
return value.
- Used AtomicReference instead of synchronizing resetMucOptions
because joinMuc is run on a directExecutor associated with the
XmppConnection thread. While the synchronized getMucOptions was
called right after this in the existing code, the documentation for
directExecutor has this to say:
When a ListenableFuture listener is registered to run under
directExecutor, the listener can execute in any of three possible
threads:
- When a thread attaches a listener to a ListenableFuture that's
already complete, the listener runs immediately in that thread.
- When a thread attaches a listener to a ListenableFuture that's
incomplete and the ListenableFuture later completes normally,
the listener runs in the thread that completes the ListenableFuture.
- When a listener is attached to a ListenableFuture and the
ListenableFuture gets cancelled, the listener runs immediately
in the thread that cancelled the Future.
- (This is applicable, since directExecutor uses in
XmppConnectionService are extensively attached to ListenableFuture)
The docs continue:
A specific warning about locking: Code that executes user-supplied
tasks, such as ListenableFuture listeners, should take care not to
do so while holding a lock. Additionally, as a further line of
defense, prefer not to perform any locking inside a task that will
be run under directExecutor: Not only might the wait for a lock
be long, but if the running thread was holding a lock, the
listener may deadlock or break lock isolation.
- Altogether, AtomicReference seems much more aligned with how Google
intends directExecutor to be used.
Anyway, here's the stacktrace that reported this error:
```
2026-02-20 10:41:09.089 7610-7610 AndroidRuntime com.cheogram.android E FATAL EXCEPTION: main (Ask Gemini)
Process: com.cheogram.android, PID: 7610
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String eu.siacs.conversations.entities.MucOptions.getName()' on a null object reference
at eu.siacs.conversations.entities.Conversation.getName(Conversation.java:1068)
at eu.siacs.conversations.entities.Conversation.getAvatarName(Conversation.java:1784)
at eu.siacs.conversations.ui.util.AvatarWorkerTask.setContentDescription(AvatarWorkerTask.java:135)
at eu.siacs.conversations.ui.util.AvatarWorkerTask.loadAvatar(AvatarWorkerTask.java:105)
at eu.siacs.conversations.ui.adapter.ConversationAdapter.onBindViewHolder(ConversationAdapter.java:255)
at eu.siacs.conversations.ui.adapter.ConversationAdapter.onBindViewHolder(ConversationAdapter.java:33)
at androidx.recyclerview.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:7846)
at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7953)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:6742)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:7013)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6853)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6849)
at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2422)
at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1722)
at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1682)
at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:747)
at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4737)
at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:4459)
at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:5011)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1891)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1729)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1638)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at androidx.coordinatorlayout.widget.CoordinatorLayout.layoutChild(CoordinatorLayout.java:1213)
at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayoutChild(CoordinatorLayout.java:899)
at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:919)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1891)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1729)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1638)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at androidx.drawerlayout.widget.DrawerLayout.onLayout(DrawerLayout.java:1273)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:25626)
at android.view.ViewGroup.layout(ViewGroup.java:6460)
```
@@ -87,8 +87,7 @@ public class MultiUserChatManager extends AbstractManager {
if (Config.MUC_LEAVE_BEFORE_JOIN) {
unavailable(conversation);
}
- conversation.resetMucOptions();- conversation.getMucOptions().setAutoPushConfiguration(autoPushConfiguration);
+ conversation.resetMucOptions().setAutoPushConfiguration(autoPushConfiguration);
conversation.setHasMessagesLeftOnServer(false);
final var disco = fetchDiscoInfo(conversation);