Skip to content

Conversation

@conroy-ricketts
Copy link
Contributor

CCCT-1867

Technical Summary

This crash happens when the user goes to the Connect Message Fragment screen, the app begins to asynchronously fetch messages, and then the user goes to another screen (e.g. hitting the back button once or twice) before the asynchronous work is finished (in this case, the work finishes with a failure). This crash happens because the fragment's context is null when the fragment detaches from its Activity, and we are attempting to use that null context to display a Toast message to the user.

So, to fix the crash I simply added a null check for the fragment's context before we try to show the Toast message.

The reason I chose this solution is because it doesn't make sense to me to show the user a Toast message regarding receiving messages if they're on a different screen entirely, which leads to poor UX. Whenever the user returns to the Connect Message Fragment Screen, the app will try to fetch the messages again anyways. However, I'd love to hear everyone's thoughts on this.

Safety Assurance

Safety story

I verified that the app no longer crashes when following these repro steps:

  1. Turn on airplane mode.
  2. Open a message channel.
  3. Immediately hit the back button.

Here is a video example of the crash before my changes:

Screen_Recording_20251103_160708_CommCare.Debug.mp4

Here is a video example after my changes:

Screen_Recording_20251103_161151_CommCare.Debug.mp4

QA Plan

For QA, we should verify that if you follow these steps, the app does not crash:

  1. Turn on airplane mode.
  2. Open a message channel.
  3. Immediately hit the back button.
  4. Try steps 1 through 3 a few different times.

Added a null check for the fragment context to avoid the crash.
@coderabbitai
Copy link

coderabbitai bot commented Nov 3, 2025

📝 Walkthrough

Walkthrough

A null-safety improvement to the failure handling in ConnectMessageFragment's message retrieval flow. The change replaces a direct Toast call with a defensive pattern that obtains the context via getContext(), validates it against null, and only displays the Toast message if context is available.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

  • Single-file change with a straightforward null-check pattern
  • Defensive coding applied to a Toast display in error handling
  • Minimal logic density and no structural modifications

Possibly related PRs

  • PR #3111: Modifies ConnectMessageFragment's failure handling for message retrieval with Toast-based feedback and null-safe context usage.

Suggested labels

skip-integration-tests

Suggested reviewers

  • Jignesh-dimagi
  • OrangeAndGreen
  • shubham1g5

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing a crash in the Connect Message Fragment, directly matching the changeset which adds a null-check to prevent an NPE when displaying a Toast.
Description check ✅ Passed The description includes most required sections: a ticket link, technical summary explaining the crash cause and fix, safety story with verification and repro steps, and a QA plan. However, the Product Description, Feature Flag, and Automated test coverage sections are missing or incomplete.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bug/CCCT-1867-connect-message-fragment-crash

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 205d162 and 68e2b78.

📒 Files selected for processing (1)
  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📓 Common learnings
Learnt from: shubham1g5
Repo: dimagi/commcare-android PR: 0
File: :0-0
Timestamp: 2025-05-08T11:08:18.530Z
Learning: PR #3048 "Phase 4 Connect PR" introduces a substantial feature called "Connect" to the CommCare Android app, which includes messaging, job management, delivery tracking, payment processing, authentication flows, and learning modules. It follows a modern architecture using Navigation Components with three navigation graphs, segregated business logic in Manager classes, and proper database persistence.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/adapters/ConnectMessageAdapter.java:0-0
Timestamp: 2025-07-29T14:14:07.954Z
Learning: In the CommCare Android Connect messaging system (ConnectMessageAdapter.java), the team prefers to let getMessage() potentially return null and crash rather than adding defensive null checks. This follows the fail-fast philosophy used throughout the Connect feature to catch programming errors immediately during development.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3108
File: app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java:74-78
Timestamp: 2025-06-06T19:54:26.428Z
Learning: In ConnectDownloadingFragment.java and similar Connect-related code, the team prefers to let "should never happen" scenarios like null app records crash rather than add defensive null checks, following a fail-fast philosophy to catch programming errors during development.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3121
File: app/src/org/commcare/activities/CommCareSetupActivity.java:360-364
Timestamp: 2025-05-22T14:28:35.959Z
Learning: In CommCareSetupActivity.java, the call to installFragment.showConnectErrorMessage() after fragment transactions is intentionally unguarded with null checks. This follows the app's design pattern where critical error paths prefer immediate crashes over silent failures, making potential issues immediately visible during development rather than hiding them with defensive programming.
Learnt from: Jignesh-dimagi
Repo: dimagi/commcare-android PR: 0
File: :0-0
Timestamp: 2025-05-08T13:40:19.645Z
Learning: PR #3048 introduces a comprehensive messaging system in the Connect feature, implementing secure encryption using AES-GCM for message content, proper channel management with consent flows, and a well-designed UI separation between sent and received messages with real-time notification integration.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/connect/MessageManager.java:0-0
Timestamp: 2025-07-29T14:54:47.940Z
Learning: User OrangeAndGreen organizes messaging-related changes into separate PRs rather than mixing them with other Connect work, which aligns with their preference for handling code issues in separate PRs when they are not directly related to the main changes.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/fragments/connect/ConnectResultsSummaryListFragment.java:0-0
Timestamp: 2025-07-29T15:21:43.403Z
Learning: User OrangeAndGreen prefers to defer memory leak fixes (like storing Context as fields in adapters) to separate PRs rather than mixing them with Connect feature work, especially when similar issues exist elsewhere in the codebase.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3121
File: app/src/org/commcare/fragments/SelectInstallModeFragment.java:201-205
Timestamp: 2025-05-22T14:26:41.341Z
Learning: In SelectInstallModeFragment.java, the showConnectErrorMessage method intentionally omits null checks because it's called at a specific point in the startup flow where UI is guaranteed to be loaded. It's designed to crash if activity or view is null to make potential issues immediately visible rather than hiding them with defensive programming.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/fragments/connect/ConnectJobsListsFragment.java:0-0
Timestamp: 2025-07-29T14:11:36.386Z
Learning: In ConnectJobsListsFragment.java error handling for JSON parsing, if the JSONObject obj is null when passed to handleCorruptJob(), the team prefers to let the code crash rather than adding defensive null checks. This follows the fail-fast philosophy used throughout the Connect feature to catch programming errors immediately rather than masking them.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java:0-0
Timestamp: 2025-07-29T14:09:49.805Z
Learning: In the CommCare Android Connect messaging system, message retry logic is implemented at the MessageManager level via MessageManager.sendUnsentMessages(). This method is called automatically when the channel list loads (ConnectMessageChannelListFragment), and it attempts to resend any outgoing messages that are not yet confirmed. Individual message fragments do not need to implement their own retry logic.
Learnt from: pm-dimagi
Repo: dimagi/commcare-android PR: 2956
File: app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java:58-60
Timestamp: 2025-02-19T15:15:01.935Z
Learning: Error handling for message retrieval in ConnectMessageChannelListFragment's retrieveMessages callback is not required as per user preference.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3108
File: app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java:62-64
Timestamp: 2025-06-04T19:17:21.213Z
Learning: In ConnectUnlockFragment.java, the user prefers to let getArguments() potentially throw NullPointerException rather than adding null checks, as the arguments are required for proper navigation flow and their absence indicates a programming error that should fail fast.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3043
File: app/src/org/commcare/android/database/connect/models/ConnectMessagingMessageRecord.java:0-0
Timestamp: 2025-04-22T17:05:39.842Z
Learning: In ConnectMessagingMessageRecord, decryption failures are expected in some scenarios and are handled by logging the exception with Logger.exception() but continuing execution by returning null, allowing the application to gracefully handle the failure.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java:0-0
Timestamp: 2025-07-29T14:08:55.466Z
Learning: In ConnectUnlockFragment.java and similar Connect-related code, the team has agreed that JSONExceptions should crash the app by throwing RuntimeException (fail-fast approach for data contract violations), while IOExceptions should be logged and allow processing to continue (graceful handling for network/stream issues). This intentional inconsistency in error handling reflects different treatment for different types of failures.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java:0-0
Timestamp: 2025-07-29T14:10:55.131Z
Learning: In ConnectUnlockFragment.java, opportunityId values are expected to always contain valid integer strings. Integer.parseInt() should be allowed to throw NumberFormatException if invalid data is encountered, following the fail-fast philosophy used throughout the Connect feature for data contract violations.
📚 Learning: 2025-07-29T14:14:07.954Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/adapters/ConnectMessageAdapter.java:0-0
Timestamp: 2025-07-29T14:14:07.954Z
Learning: In the CommCare Android Connect messaging system (ConnectMessageAdapter.java), the team prefers to let getMessage() potentially return null and crash rather than adding defensive null checks. This follows the fail-fast philosophy used throughout the Connect feature to catch programming errors immediately during development.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-05-22T14:28:35.959Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3121
File: app/src/org/commcare/activities/CommCareSetupActivity.java:360-364
Timestamp: 2025-05-22T14:28:35.959Z
Learning: In CommCareSetupActivity.java, the call to installFragment.showConnectErrorMessage() after fragment transactions is intentionally unguarded with null checks. This follows the app's design pattern where critical error paths prefer immediate crashes over silent failures, making potential issues immediately visible during development rather than hiding them with defensive programming.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-02-19T15:15:01.935Z
Learnt from: pm-dimagi
Repo: dimagi/commcare-android PR: 2956
File: app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java:58-60
Timestamp: 2025-02-19T15:15:01.935Z
Learning: Error handling for message retrieval in ConnectMessageChannelListFragment's retrieveMessages callback is not required as per user preference.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-05-22T14:26:41.341Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3121
File: app/src/org/commcare/fragments/SelectInstallModeFragment.java:201-205
Timestamp: 2025-05-22T14:26:41.341Z
Learning: In SelectInstallModeFragment.java, the showConnectErrorMessage method intentionally omits null checks because it's called at a specific point in the startup flow where UI is guaranteed to be loaded. It's designed to crash if activity or view is null to make potential issues immediately visible rather than hiding them with defensive programming.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-05-09T10:57:41.073Z
Learnt from: Jignesh-dimagi
Repo: dimagi/commcare-android PR: 3093
File: app/res/navigation/nav_graph_connect_messaging.xml:41-45
Timestamp: 2025-05-09T10:57:41.073Z
Learning: In the CommCare Android codebase, the navigation graph for Connect messaging (`nav_graph_connect_messaging.xml`) intentionally uses `channel_id` as the argument name in the connectMessageFragment, despite using `channelId` in other parts of the same navigation graph. This naming difference is by design in the refactored code.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-06-04T19:17:21.213Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3108
File: app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java:62-64
Timestamp: 2025-06-04T19:17:21.213Z
Learning: In ConnectUnlockFragment.java, the user prefers to let getArguments() potentially throw NullPointerException rather than adding null checks, as the arguments are required for proper navigation flow and their absence indicates a programming error that should fail fast.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-06-06T19:54:26.428Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3108
File: app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java:74-78
Timestamp: 2025-06-06T19:54:26.428Z
Learning: In ConnectDownloadingFragment.java and similar Connect-related code, the team prefers to let "should never happen" scenarios like null app records crash rather than add defensive null checks, following a fail-fast philosophy to catch programming errors during development.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-05-08T11:08:18.530Z
Learnt from: shubham1g5
Repo: dimagi/commcare-android PR: 0
File: :0-0
Timestamp: 2025-05-08T11:08:18.530Z
Learning: PR #3048 "Phase 4 Connect PR" introduces a substantial feature called "Connect" to the CommCare Android app, which includes messaging, job management, delivery tracking, payment processing, authentication flows, and learning modules. It follows a modern architecture using Navigation Components with three navigation graphs, segregated business logic in Manager classes, and proper database persistence.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-07-29T14:09:49.805Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java:0-0
Timestamp: 2025-07-29T14:09:49.805Z
Learning: In the CommCare Android Connect messaging system, message retry logic is implemented at the MessageManager level via MessageManager.sendUnsentMessages(). This method is called automatically when the channel list loads (ConnectMessageChannelListFragment), and it attempts to resend any outgoing messages that are not yet confirmed. Individual message fragments do not need to implement their own retry logic.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-04-18T20:13:29.655Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3040
File: app/src/org/commcare/android/database/connect/models/ConnectJobDeliveryFlagRecord.java:39-55
Timestamp: 2025-04-18T20:13:29.655Z
Learning: In the CommCare Android Connect feature, the JSON object passed to `ConnectJobDeliveryFlagRecord.fromJson()` method should never be null based on the implementation design.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
📚 Learning: 2025-06-06T19:52:53.173Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3108
File: app/src/org/commcare/fragments/connect/ConnectDeliveryDetailsFragment.java:163-180
Timestamp: 2025-06-06T19:52:53.173Z
Learning: In the CommCare Android Connect feature, database operations like ConnectJobUtils.upsertJob should be allowed to crash rather than being wrapped in try-catch blocks. The team prefers fail-fast behavior for database errors instead of graceful error handling.

Applied to files:

  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Lint Code Base
🔇 Additional comments (1)
app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1)

111-118: LGTM! Crash fix is correct and consistent.

The null check appropriately prevents the crash when the async callback fires after fragment detachment. This defensive pattern is consistent with refreshUi() (line 201-202) and reasonable for async timing scenarios. Silently skipping the Toast when the user has navigated away is the right behavior.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@conroy-ricketts conroy-ricketts added the skip-integration-tests Skip android tests. label Nov 3, 2025
refreshUi();
} else {
Toast.makeText(requireContext(), getString(R.string.connect_messaging_retrieve_messages_fail), Toast.LENGTH_SHORT).show();
Context context = getContext();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will actually not solve the real problem but suppress it.

I think issue is here, it has delay of 30 minutes before calling API, which is causing this issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a real problem to suppress though is what I mean.

The API is being called every 30 seconds while the user is on the Connect Message Fragment screen. The current crash happens when the API is called, and then the user moves to a different screen before the work calling the API finishes, and that's why the context would be null.

I feel like we should not want to update the UI if the current screen the user is looking at is irrelevant right?

Looking at this code, we are already following this same philosophy in the function refreshUi() in this class:

        Context context = getContext();
        if (context != null) {
            List<ConnectMessagingMessageRecord> messages = ConnectMessagingDatabaseHelper.getMessagingMessagesForChannel(context, channelId);

            List<ConnectMessageChatData> chats = new ArrayList<>();
            for (ConnectMessagingMessageRecord message : messages) {

                chats.add(fromMessage(message));

                if (!message.getUserViewed()) {
                    message.setUserViewed(true);
                    ConnectMessagingDatabaseHelper.storeMessagingMessage(context, message);
                }
            }

            adapter.updateData(chats);
            scrollToLatestMessage();

        }

Checking if the context is not null makes more sense to me because why would we want to update the UI if the fragment is detached from its activity?

I'm open for discussion though

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@conroy-ricketts I feel that 30 seconds is too high, may be some one wanted to put 3 seconds but added extra zero there.
If user is not on the screen, I think it should not call the API only. Can we cancel the handler in onDestroyView?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something, but the size of the interval does not cause the crash, and cancelling the handler in onDestroyView will not solve the issue. The crash happens because the coroutine in PushNotificationApiHelper's retrieveLatestPushNotificationsWithCallback() is launched while the user is on the Connect Message Fragment, but finishes and calls a listener when the user is no longer on the fragment:

object PushNotificationApiHelper {

    fun retrieveLatestPushNotificationsWithCallback(
        context: Context,
        listener: ConnectActivityCompleteListener
    ) {
        CoroutineScope(Dispatchers.IO).launch {
            retrieveLatestPushNotifications(context).onSuccess {
                withContext(Dispatchers.Main) { //  switching to main to touch views
                    listener.connectActivityComplete(true)
                }
            }.onFailure {
                withContext(Dispatchers.Main) { //  switching to main to touch views
                    listener.connectActivityComplete(false)
                }
            }
        }
    }

This coroutine runs very fast normally.

For example, the crash will not happen if you wait a few seconds before hitting the back button on the Connect Message Fragment screen. The crash only happens when you hit the back button while the coroutine is doing its work

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that this line is calling the Runnable again after 30 seconds and as user not on this fragment, causing issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I noticed too is that we are already cancelling the handler here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 30 second refresh rate was chosen intentionally as a way to have messages refresh somewhat frequently without constantly hitting the network. It was originally 60 seconds but we cut it in half earlier this year.

Also, it looks like the handler is already cleaned up during onPause so the call stops happening when the user leaves the page.

All in all, I think Conroy's fix is what we need here since the call can start while the user is on the page but take a while to complete such that the user may have navigated away by the time the call completes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it looks like Conroy's fix is what we need. This is strange as we need to handle manually the context. We need ViewModel scope here soon.

I was also just trying to understand why app is not crashing whenever it has valid response but only when it fails. Answer is here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! I was worried about the success case and verified the same. Explains why all the crash logs were for failed calls.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am good with the solution here until we correct lifecyle management of network calls from UI. The alternative here would be to bind the callback to fragment lifecyle so that it never gets called if fragment is no longer alive. Although that might require a lot more changes (like doing this call from a view model scoped to fragment lifecycle). I do want to call out the downside of current solution that it will not hard crash in cases when context is null here due to some bad code (for example calling fetchMessagesFromNetwork from onDestroy, It will never pass our code reviews though :D).

The 30 second refresh rate was chosen intentionally as a way to have messages refresh somewhat frequently without constantly hitting the network. It was originally 60 seconds but we cut it in half earlier this year.

Is there context on that decision somewhere ? 30 seconds does still seem high to me.

Copy link
Contributor

@OrangeAndGreen OrangeAndGreen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A+ on the PR description and demo videos!

@conroy-ricketts conroy-ricketts merged commit 1793878 into master Nov 4, 2025
7 checks passed
@conroy-ricketts conroy-ricketts deleted the bug/CCCT-1867-connect-message-fragment-crash branch November 4, 2025 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants