From 724355ba926789c0face89a7aa42fe6973bd8f49 Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Wed, 1 Apr 2026 21:44:49 -0400 Subject: [PATCH] fix crash when clicking invisible footer the stack trace manifested as: ``` Version: Cheogram debug-local+free Manufacturer: OnePlus Device: OnePlus6 Timestamp: 2026-04-01 21:27:32 -0400 java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1 at jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64) at jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70) at jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266) at java.util.Objects.checkIndex(Objects.java:385) at java.util.ArrayList.get(ArrayList.java:434) at eu.siacs.conversations.ui.ManageAccountActivity.lambda$onCreate$3(ManageAccountActivity.java:140) at eu.siacs.conversations.ui.ManageAccountActivity.$r8$lambda$fcEx2bGWBsQPp04OkU82pOGEvUg(Unknown Source:0) at eu.siacs.conversations.ui.ManageAccountActivity$$ExternalSyntheticLambda0.onItemClick(D8$$SyntheticClass:0) at android.widget.AdapterView.performItemClick(AdapterView.java:330) at android.widget.AbsListView.performItemClick(AbsListView.java:1274) at android.widget.AbsListView.onKeyUp(AbsListView.java:3511) at android.widget.ListView.commonKey(ListView.java:2498) at android.widget.ListView.onKeyUp(ListView.java:2371) at android.view.KeyEvent.dispatch(KeyEvent.java:3471) at android.view.View.dispatchKeyEvent(View.java:16521) at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1982) at android.widget.ListView.dispatchKeyEvent(ListView.java:2346) at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1987) at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1987) at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1987) at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1987) at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1987) at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1987) at com.android.internal.policy.DecorView.superDispatchKeyEvent(DecorView.java:462) at com.android.internal.policy.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1971) at android.app.Activity.dispatchKeyEvent(Activity.java:4557) at androidx.core.app.ComponentActivity.superDispatchKeyEvent(ComponentActivity.kt:96) at androidx.core.view.KeyEventDispatcher.dispatchKeyEvent(KeyEventDispatcher.java:85) at androidx.core.app.ComponentActivity.dispatchKeyEvent(ComponentActivity.kt:110) at androidx.appcompat.app.AppCompatActivity.dispatchKeyEvent(AppCompatActivity.java:604) at androidx.appcompat.view.WindowCallbackWrapper.dispatchKeyEvent(WindowCallbackWrapper.java:59) at androidx.appcompat.app.AppCompatDelegateImpl$AppCompatWindowCallback.dispatchKeyEvent(AppCompatDelegateImpl.java:3397) at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:376) at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:8034) at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:7873) at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7259) at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7316) at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7282) at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:7448) at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7290) at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:7505) at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7263) at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7316) at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7282) at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:7290) at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:7263) at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:7316) at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:7282) at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:7481) at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:7721) at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:5053) at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:4430) at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:4421) at android.view.inputmethod.InputMethodManager.-$$Nest$mfinishedInputEvent(Unknown Source:0) at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:5030) at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:181) at android.os.MessageQueue.nativePollOnce(Native Method) at android.os.MessageQueue.next(MessageQueue.java:387) at android.os.Looper.loopOnce(Looper.java:189) at android.os.Looper.loop(Looper.java:317) at android.app.ActivityThread.main(ActivityThread.java:8934) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:591) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911) ``` notice that this required tab navigation, as the footer doesn't seem visible, although maybe possible to hit it with your thumb idk. this is especially tricky when the footer is invisible, i.e., when there aren't any PSTN gateways configured --- .../ui/ManageAccountActivity.java | 11 +++- .../ui/ManageAccountActivityTest.java | 66 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/test/java/eu/siacs/conversations/ui/ManageAccountActivityTest.java diff --git a/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java index c409b2facdf5f38ea395a5e192ecbc64a065937c..de54b67aaf34f51851c26332357387bf261fb65f 100644 --- a/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/cheogram/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -136,7 +136,12 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda accountListView = binding.accountList; this.mAccountAdapter = new AccountAdapter(this, accountList); accountListView.setAdapter(this.mAccountAdapter); - accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position))); + accountListView.setOnItemClickListener((arg0, view, position, arg3) -> { + final Object item = arg0.getItemAtPosition(position); + if (item != null) { + switchToAccount((Account) item); + } + }); LayoutInflater inflater = getLayoutInflater(); @@ -164,7 +169,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda ManageAccountActivity.this.getMenuInflater().inflate( R.menu.manageaccounts_context, menu); AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; - this.selectedAccount = accountList.get(acmi.position); + final Object item = accountListView.getItemAtPosition(acmi.position); + if (item == null) return; + this.selectedAccount = (Account) item; if (this.selectedAccount.isEnabled()) { menu.findItem(R.id.mgmt_account_enable).setVisible(false); menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp()); diff --git a/src/test/java/eu/siacs/conversations/ui/ManageAccountActivityTest.java b/src/test/java/eu/siacs/conversations/ui/ManageAccountActivityTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e63836935eb5a563eb40f1f364e125c844cb9349 --- /dev/null +++ b/src/test/java/eu/siacs/conversations/ui/ManageAccountActivityTest.java @@ -0,0 +1,66 @@ +package eu.siacs.conversations.ui; + +import android.content.Intent; +import android.os.Build; +import android.widget.AdapterView; +import android.widget.ListView; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.ConscryptMode; +import org.robolectric.shadows.ShadowActivity; + +import eu.siacs.conversations.Conversations; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.Jid; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.TIRAMISU, application = Conversations.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class ManageAccountActivityTest { + + @Test + public void onItemClickNavigatesToCorrectAccountAndIgnoresFooter() { + ManageAccountActivity activity = + Robolectric.buildActivity(ManageAccountActivity.class).create().get(); + + Account[] accounts = new Account[] { + new Account(Jid.ofLocalAndDomain("alice", "example.org"), "password"), + new Account(Jid.ofLocalAndDomain("bob", "example.org"), "password"), + new Account(Jid.ofLocalAndDomain("carol", "example.org"), "password"), + }; + for (Account account : accounts) { + activity.accountList.add(account); + } + activity.mAccountAdapter.notifyDataSetChanged(); + + ListView listView = activity.accountListView; + AdapterView.OnItemClickListener listener = listView.getOnItemClickListener(); + ShadowActivity shadow = Shadows.shadowOf(activity); + + for (int position = 0; position < accounts.length; position++) { + listener.onItemClick(listView, null, position, 0); + Intent started = shadow.getNextStartedActivity(); + Assert.assertNotNull("position " + position + ": should start activity", started); + Assert.assertEquals( + "position " + position + ": should navigate to correct account", + accounts[position].getJid().asBareJid().toString(), + started.getStringExtra("jid")); + } + + int footerPosition = accounts.length; + Assert.assertEquals( + "position " + footerPosition + ": should be a header/footer view type", + AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER, + listView.getAdapter().getItemViewType(footerPosition)); + listener.onItemClick(listView, null, footerPosition, 0); + Assert.assertNull( + "position " + footerPosition + " (footer): should not start activity", + shadow.getNextStartedActivity()); + } +}