Skip to content

Conversation

@jaypanchal-13
Copy link
Contributor

@jaypanchal-13 jaypanchal-13 commented Sep 12, 2025

Crash Description

Ticket -> https://dimagi.atlassian.net/browse/CCCT-1620

      Fatal Exception: java.lang.IllegalStateException: FragmentManager is already executing transactions
   at androidx.fragment.app.FragmentManager.ensureExecReady(FragmentManager.java:1717)
   at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1776)
   at androidx.fragment.app.FragmentManager.executePendingTransactions(FragmentManager.java:641)
   at androidx.biometric.BiometricPrompt.findOrAddBiometricFragment(BiometricPrompt.java:1082)
   at androidx.biometric.BiometricPrompt.authenticateInternal(BiometricPrompt.java:992)
   at androidx.biometric.BiometricPrompt.authenticate(BiometricPrompt.java:972)
   at org.commcare.utils.BiometricsHelper.authenticatePinOrBiometric(BiometricsHelper.java:149)
   at org.commcare.utils.BiometricsHelper.authenticatePin(BiometricsHelper.java:127)
   at org.commcare.connect.PersonalIdManager.unlockConnect(PersonalIdManager.java:192)
   at org.commcare.fragments.connect.ConnectUnlockFragment.onCreateView(ConnectUnlockFragment.java:71)
   at androidx.fragment.app.Fragment.performCreateView(Fragment.java:3114)
   at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:557)
   at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:272)
   at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1943)
   at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1845)
   at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1782)
   at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3042)
   at androidx.fragment.app.FragmentManager.dispatchViewCreated(FragmentManager.java:2945)
   at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:3148)
   at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:588)
   at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:272)
   at androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:114)
   at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1455)
   at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3034)
   at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2952)
   at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:263)
   at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:350)
   at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:210)
   at org.commcare.activities.CommCareActivity.onStart(CommCareActivity.java:290)
   at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1455)
   at android.app.Activity.performStart(Activity.java:8085)
   at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3708)
   at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:222)
   at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:202)
   at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:174)
   at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:98)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2252)
   at android.os.Handler.dispatchMessage(Handler.java:106)
   at android.os.Looper.loopOnce(Looper.java:201)
   at android.os.Looper.loop(Looper.java:288)
   at android.app.ActivityThread.main(ActivityThread.java:7941)
   at java.lang.reflect.Method.invoke(Method.java)
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:553)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

RCA

The crash was caused because BiometricPrompt.authenticate() adds an internal fragment to FragmentManager. We were calling it inside onCreateView(), while FragmentManager was still inflating ConnectUnlockFragment, leading to:
IllegalStateException: FragmentManager is already executing transactions. the issue was timing of the call

Solution

By moving the call into onViewCreated(), we ensure the fragment’s view is fully created and FragmentManager is idle before biometric is triggered. This avoids transaction conflicts.

Feature Flag

Safety Assurance

Safety story

Automated test coverage

QA Plan

Labels and Review

  • Do we need to enhance the manual QA test coverage ? If yes, the "QA Note" label is set correctly
  • Does the PR introduce any major changes worth communicating ? If yes, the "Release Note" label is set and a "Release Note" is specified in PR description.
  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

@coderabbitai
Copy link

coderabbitai bot commented Sep 12, 2025

📝 Walkthrough

Walkthrough

The unlocking logic in ConnectUnlockFragment was moved from onCreateView to onViewCreated. onCreateView now only inflates and returns the view. A new onViewCreated override triggers PersonalIdManager.getInstance().unlockConnect with callbacks: on success, retrieveOpportunities is executed; on failure, the hosting activity is finished. No other logic changes or navigation alterations were introduced. One new public method signature was added: onViewCreated(@nonnull View, @nullable Bundle).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor OS as Android OS
    participant F as ConnectUnlockFragment
    participant P as PersonalIdManager
    participant A as Activity

    OS->>F: onCreateView()
    Note right of F: Inflate view and return\n(binding.getRoot())
    OS->>F: onViewCreated(view, savedInstanceState)
    F->>P: unlockConnect(callback)

    alt unlock success
        P-->>F: onSuccess
        F->>F: retrieveOpportunities()
        Note right of F: Subsequent navigation\nunchanged
    else unlock failure
        P-->>F: onFailure
        F->>A: finish()
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

skip-integration-tests

Suggested reviewers

  • OrangeAndGreen
  • shubham1g5
  • Jignesh-dimagi

Pre-merge checks (1 passed, 2 warnings)

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The current description contains a detailed crash stacktrace, RCA, solution, and a ticket link, but it does not follow the repository template: the Product Description and Technical Summary headers are not clearly structured, Feature Flag is unspecified, and the Safety Assurance sections (safety story, automated test coverage) and QA Plan are left as placeholders, so required information is incomplete for a merge review. Please update the PR description to follow the provided template: add a concise Product Description of user-facing impact, move the ticket link and design rationale into Technical Summary, state the Feature Flag (or "none"), fill in the Safety Assurance section with a short safety story and any automated test coverage, and provide a QA Plan and completed labels/checklist entries so reviewers can verify risk and testing.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Solved crash FragmentManager is already executing transactions" succinctly describes the primary fix in this changeset (resolving the IllegalStateException caused by starting biometric auth during fragment view creation) and is directly related to the main change, so it is clear and actionable for reviewers scanning history.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch CCCT-1620-biometric-helper-crash

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (1)

31-36: Null out view binding in onDestroyView to avoid leaks (team convention).

This fragment uses view binding but doesn’t clear it. Add the standard cleanup.

 class ConnectUnlockFragment extends Fragment {
     private FragmentConnectUnlockBinding binding;
@@
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
     }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        binding = null;
+    }
🧹 Nitpick comments (4)
app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (4)

61-71: Post biometric launch to the next frame to fully exit FragmentManager’s transaction.

onViewCreated can still run while FragmentManager is executing the add/replace transaction. Post to the view to guarantee we’re outside that critical section and guard for detachment.

Apply:

-    PersonalIdManager.getInstance().unlockConnect((CommCareActivity<?>) requireActivity(), success -> {
-        if (success) {
-            retrieveOpportunities();
-        } else {
-            requireActivity().finish();
-        }
-    });
+    view.post(() -> {
+        if (!isAdded()) { return; }
+        CommCareActivity<?> activity = (CommCareActivity<?>) requireActivity();
+        PersonalIdManager.getInstance().unlockConnect(activity, success -> {
+            if (!isAdded()) { return; }
+            if (success) {
+                retrieveOpportunities();
+            } else if (!requireActivity().isFinishing() && !requireActivity().isDestroyed()) {
+                requireActivity().finish();
+            }
+        });
+    });

Please re-check the original crash path on a few devices/AndroidX versions after this tweak.


73-92: Guard async callbacks against detached fragment before navigating.

Network callbacks can return after view destruction. Add a quick attachment check before redirection to prevent IllegalStateException.

     private void retrieveOpportunities() {
         ConnectUserRecord user = ConnectUserDatabaseUtil.getUser(getContext());
         new ConnectApiHandler<ConnectOpportunitiesResponseModel>() {
@@
             @Override
             public void onSuccess(ConnectOpportunitiesResponseModel data) {
                 if (!data.getValidJobs().isEmpty()) {
                     ConnectJobUtils.storeJobs(requireContext(), data.getValidJobs(), true);
                     ConnectUserDatabaseUtil.turnOnConnectAccess(requireContext());
                 }
-                setFragmentRedirection();
+                if (isAdded()) {
+                    setFragmentRedirection();
+                }
             }
         }.getConnectOpportunities(requireContext(), user);
     }

74-74: Prefer requireContext() here.

Avoid possible null; consistent with the rest of the file.

-        ConnectUserRecord user = ConnectUserDatabaseUtil.getUser(getContext());
+        ConnectUserRecord user = ConnectUserDatabaseUtil.getUser(requireContext());

99-130: Navigation: remove redundant popBackStack and decouple from binding root.

popUpTo(inclusive=true) already clears back stack; the extra popBackStack is unnecessary. Also prefer NavHostFragment.findNavController(this) to avoid reliance on a non-null view/binding.

-        NavController navController = Navigation.findNavController(binding.getRoot());
-        navController.popBackStack();
+        NavController navController = NavHostFragment.findNavController(this);
         NavOptions options = new NavOptions.Builder()
                 .setPopUpTo(navController.getGraph().getStartDestinationId(), true, true)
                 .build();
         navController.navigate(fragmentId, bundle, options);

Add import:

import androidx.navigation.fragment.NavHostFragment;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbf2af1 and 0f6043d.

📒 Files selected for processing (1)
  • app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: OrangeAndGreen
PR: dimagi/commcare-android#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
PR: dimagi/commcare-android#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.
Learnt from: pm-dimagi
PR: dimagi/commcare-android#2949
File: app/src/org/commcare/fragments/connectId/ConnectIdBiometricConfigFragment.java:235-236
Timestamp: 2025-02-04T21:29:29.594Z
Learning: The empty performPasswordUnlock method in ConnectIdBiometricConfigFragment is intentionally left empty and should not be flagged in reviews.
📚 Learning: 2025-06-04T19:17:21.213Z
Learnt from: OrangeAndGreen
PR: dimagi/commcare-android#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/connect/ConnectUnlockFragment.java
📚 Learning: 2025-07-29T14:10:55.131Z
Learnt from: OrangeAndGreen
PR: dimagi/commcare-android#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.

Applied to files:

  • app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java
📚 Learning: 2025-02-04T21:29:29.594Z
Learnt from: pm-dimagi
PR: dimagi/commcare-android#2949
File: app/src/org/commcare/fragments/connectId/ConnectIdBiometricConfigFragment.java:235-236
Timestamp: 2025-02-04T21:29:29.594Z
Learning: The empty performPasswordUnlock method in ConnectIdBiometricConfigFragment is intentionally left empty and should not be flagged in reviews.

Applied to files:

  • app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java
📚 Learning: 2025-03-10T08:16:59.436Z
Learnt from: shubham1g5
PR: dimagi/commcare-android#2949
File: app/src/org/commcare/fragments/connectId/ConnectIDSecondaryPhoneNumber.java:58-59
Timestamp: 2025-03-10T08:16:59.436Z
Learning: All fragments using view binding should implement proper cleanup in onDestroyView() by setting binding to null to prevent memory leaks.

Applied to files:

  • app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java
🧬 Code graph analysis (1)
app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (1)
app/src/org/commcare/connect/PersonalIdManager.java (1)
  • PersonalIdManager (64-621)
⏰ 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/connect/ConnectUnlockFragment.java (1)

58-59: Good move: stop triggering BiometricPrompt in onCreateView.

Returning the view here and deferring auth out of onCreateView directly addresses the FragmentManager “already executing transactions” crash.

@shubham1g5
Copy link
Contributor

@jaypanchal-13 Think Jignesh was able to repro this already on clicking a notification and is addressing this as part of this

@shubham1g5 shubham1g5 closed this Sep 15, 2025
@shubham1g5 shubham1g5 deleted the CCCT-1620-biometric-helper-crash branch September 15, 2025 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants