Skip to content

Conversation

@OrangeAndGreen
Copy link
Contributor

Product Description

After entering the wrong backup code too many times when trying to recover an account, the user will be locked out by the server and shown a message in mobile informing them.

Technical Summary

Handling failure case when server returns an error_code instead of attempts_left.
Mobile should no longer proceed to the "create backup code" step in this scenario, and should instead exit the workflow.

Feature Flag

PersonalID

@coderabbitai
Copy link

coderabbitai bot commented Jun 24, 2025

📝 Walkthrough

Walkthrough

This update introduces a comprehensive set of features and enhancements centered around a new "Connect" module within the application. Major changes include the addition of new activities (ConnectActivity, ConnectMessagingActivity), fragments, adapters, and supporting classes to enable Connect job management, messaging, delivery tracking, and payment acknowledgment. The AndroidManifest is updated to register these new components and a broadcast receiver for payment acknowledgments. Extensive UI updates are made, including new layouts, menu items, drawables, and localized strings for multiple languages. Backend logic is added for managing Connect jobs, messaging channels, and secure communication, with new database helpers and network API integrations. Notification handling is refactored to support Connect-specific actions and messaging updates. Exception handling and transaction management are refined in several utility classes. The build configuration and test application setup are also updated for consistency and improved testability.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App
    participant ConnectActivity
    participant ConnectManager
    participant Server
    participant ConnectMessagingActivity
    participant Database
    participant PaymentAcknowledgeReceiver

    %% Connect Job Flow
    User->App: Launches ConnectActivity
    App->ConnectActivity: Start
    ConnectActivity->ConnectManager: Initialize and fetch active job
    ConnectManager->Server: Fetch job opportunities / progress
    Server-->>ConnectManager: Job data
    ConnectManager->Database: Store/update job records
    ConnectActivity->App: Display job info, progress, actions

    %% Messaging Flow
    User->ConnectMessagingActivity: Opens Messaging
    ConnectMessagingActivity->ConnectManager: Fetch channels/messages
    ConnectManager->Server: Retrieve messages/channels
    Server-->>ConnectManager: Messaging data
    ConnectManager->Database: Store messages/channels
    ConnectMessagingActivity->App: Display channels/messages

    %% Sending a Message
    User->ConnectMessagingActivity: Send message
    ConnectMessagingActivity->ConnectManager: Send message
    ConnectManager->Server: Send encrypted message
    Server-->>ConnectManager: Acknowledge/confirm
    ConnectManager->Database: Update message status

    %% Payment Acknowledgment via Notification
    Server->App: Sends FCM payment notification
    App->PaymentAcknowledgeReceiver: Receives broadcast
    PaymentAcknowledgeReceiver->ConnectManager: Update delivery progress
    ConnectManager->Server: Update payment status
    Server-->>ConnectManager: Confirmation
    ConnectManager->Database: Update payment record

    %% UI Update on Notification
    PaymentAcknowledgeReceiver->App: Broadcast UI update
    App->ConnectActivity: Refresh payment/delivery UI
Loading

Possibly related PRs

Suggested labels

skip-integration-tests

Suggested reviewers

  • pm-dimagi
  • Jignesh-dimagi
  • shubham1g5
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate Unit Tests
  • Create PR with Unit Tests
  • Post Copyable Unit Tests in Comment
  • Commit Unit Tests in branch dv/personalid_lockout

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai auto-generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 66

♻️ Duplicate comments (1)
app/src/org/commcare/android/database/connect/models/ConnectJobLearningRecord.java (1)

53-53: Verify consistency with DateUtils.parseDateTime exception handling.

Similar to ConnectJobAssessmentRecord, this method signature was updated to remove ParseException but still calls DateUtils.parseDateTime() on line 59. This change should be consistent with the underlying DateUtils implementation.

🧹 Nitpick comments (36)
app/src/org/commcare/connect/network/parser/ConfirmBackupCodeResponseParser.java (1)

26-28: Good addition for PersonalID lockout handling.

This change correctly implements the lockout detection logic mentioned in the PR objectives. The parser now captures server-side error codes that indicate account lockout scenarios.

Consider adding type validation for robustness:

 if (json.has("error_code")) {
-    sessionData.setSessionFailureCode(json.getString("error_code"));
+    sessionData.setSessionFailureCode(JsonExtensions.optStringSafe(json, "error_code", null));
 }
app/src/org/commcare/connect/IConnectAppLauncher.java (1)

3-5: Consider adding documentation for better interface usability.

The interface design is clean and follows single responsibility principle. However, adding Javadoc would improve maintainability:

+/**
+ * Interface for launching Connect applications with contextual information.
+ */
 public interface IConnectAppLauncher {
+    /**
+     * Launches a Connect application.
+     * @param appId the unique identifier of the app to launch
+     * @param isLearning whether the app should be launched in learning mode
+     */
     void launchApp(String appId, boolean isLearning);
 }
app/src/org/commcare/connect/database/ConnectDatabaseUtils.java (1)

68-70: Inconsistent exception handling - consider using specific exceptions.

The getConnectDbEncodedPassphrase method still uses generic Exception catching, which is inconsistent with the refinements made in other methods. Consider updating this to catch specific exceptions like EncryptionUtils.EncryptionException and Base64DecoderException for consistency.

-        } catch (Exception e) {
-            Logger.exception("Getting DB passphrase", e);
+        } catch (Base64DecoderException | EncryptionUtils.EncryptionException e) {
+            Logger.exception("Getting DB passphrase", e);
app/src/org/commcare/adapters/ChannelAdapter.java (3)

69-77: Optimize message processing for better performance.

The current loop through all messages to find the latest timestamp and count unread messages could be inefficient for channels with many messages. Consider caching these values in the database or using more efficient queries.

+            // Consider caching these values in the database for better performance
             Date lastDate = null;
             int unread = 0;
             for(ConnectMessagingMessageRecord message : channel.getMessages()) {
                 if(lastDate == null || lastDate.before(message.getTimeStamp())) {
                     lastDate = message.getTimeStamp();
                 }
 
                 if(!message.getUserViewed()) {
                     unread++;
                 }
             }

88-89: Cache SimpleDateFormat for better performance.

Creating a new SimpleDateFormat instance for each binding is expensive. Consider using a static final instance or DateTimeFormatter.

+    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd MMM, yyyy", Locale.ENGLISH);
+    
     // In the bind method:
-                    SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMM, yyyy", Locale.ENGLISH);
-                    lastText = outputFormat.format(lastDate);
+                    lastText = DATE_FORMAT.format(lastDate);

34-37: Consider using DiffUtil for better performance.

Using notifyDataSetChanged() is inefficient as it refreshes the entire list. Consider implementing DiffUtil for more granular updates.

Consider implementing a DiffUtil.Callback to efficiently update only changed items:

public void setChannels(List<ConnectMessagingChannelRecord> newChannels) {
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new ChannelDiffCallback(this.channels, newChannels));
    this.channels = newChannels;
    diffResult.dispatchUpdatesTo(this);
}
app/src/org/commcare/fragments/connectMessaging/ConnectMessageChatData.java (1)

15-21: Consider initializing all fields in constructor.

The constructor initializes most fields but leaves countUnread with its default value of 0. For consistency and clarity, consider either adding it as a parameter or documenting this behavior.

If countUnread should always start at 0, consider adding a comment:

 // Constructor with parameters
-public ConnectMessageChatData(int type, String message, String userName, Date timestamp, boolean isMessageRead) {
+public ConnectMessageChatData(int type, String message, String userName, Date timestamp, boolean isMessageRead) {
+    // countUnread initialized to 0 by default
     this.type = type;

Alternatively, include it as a parameter if it should be configurable:

-public ConnectMessageChatData(int type, String message, String userName, Date timestamp, boolean isMessageRead) {
+public ConnectMessageChatData(int type, String message, String userName, Date timestamp, boolean isMessageRead, int countUnread) {
     this.type = type;
     this.message = message;
     this.userName = userName;
     this.timestamp = timestamp;
     this.isMessageRead = isMessageRead;
+    this.countUnread = countUnread;
 }
app/src/org/commcare/adapters/ConnectProgressJobSummaryAdapter.java (1)

35-42: Improve string formatting and consider performance optimizations.

The adapter implementation is solid with proper division-by-zero protection. However, consider these improvements:

  1. Use string formatting instead of concatenation for better performance and localization
  2. Consider caching the percentage calculation if the data doesn't change frequently

Apply this diff for better string formatting:

-holder.tvPrimaryVisitCount.setText(String.valueOf(primaryVisit.getPaymentUnitAmount()) + "/" + String.valueOf(primaryVisit.getPaymentUnitMaxDaily()));
+holder.tvPrimaryVisitCount.setText(String.format(Locale.getDefault(), "%d/%d", 
+    primaryVisit.getPaymentUnitAmount(), primaryVisit.getPaymentUnitMaxDaily()));

Don't forget to add the import:

+import java.util.Locale;
app/AndroidManifest.xml (1)

151-153: Consider adding intent filters for ConnectMessagingActivity if needed.

The ConnectMessagingActivity is declared but doesn't have any intent filters. If this activity should be launchable from external intents or handle specific actions, consider adding appropriate intent filters.

If this activity needs to handle specific intents, add intent filters like:

 <activity
     android:name="org.commcare.activities.connect.ConnectMessagingActivity">
+    <intent-filter>
+        <action android:name="org.commcare.action.CONNECT_MESSAGING" />
+        <category android:name="android.intent.category.DEFAULT" />
+    </intent-filter>
 </activity>
app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.java (1)

166-166: Fix formatting issue.

Add space after if for consistent code style.

-if(showHours) {
+if (showHours) {
app/src/org/commcare/fragments/connect/ConnectJobsListsFragment.java (2)

146-147: Address TODO about exception handling.

The TODO comment suggests removing the try-catch, but proper error handling is important for robustness. Consider keeping the try-catch but improving error reporting.

Would you like me to help refactor this to maintain proper error handling while addressing the TODO concern?


301-301: Consider using explicit case statements for clarity.

While the fall-through syntax is valid, consider making it more explicit for better readability.

-case STATUS_AVAILABLE_NEW, STATUS_AVAILABLE:
+case STATUS_AVAILABLE_NEW:
+case STATUS_AVAILABLE:
app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java (2)

87-89: Simplify complex conditional check.

The nested conditions can be simplified for better readability.

-if(getActivity()!=null && getActivity() instanceof CommCareActivity && ((CommCareActivity)getActivity()).getSupportActionBar()!=null){
-    ((CommCareActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+CommCareActivity activity = getActivity() instanceof CommCareActivity ? (CommCareActivity) getActivity() : null;
+if (activity != null && activity.getSupportActionBar() != null) {
+    activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}

108-108: Break long line for better readability.

Split the navigation call into multiple lines.

-Navigation.findNavController(binding.rvChannel).navigate(channel.getConsented() ? getConnectMessageFragmentDirection(channel):getChannelConsetBottomSheetDirection(channel));
+Navigation.findNavController(binding.rvChannel)
+        .navigate(channel.getConsented() 
+                ? getConnectMessageFragmentDirection(channel)
+                : getChannelConsetBottomSheetDirection(channel));
app/src/org/commcare/services/PaymentAcknowledgeReceiver.java (1)

49-52: Fix Javadoc parameter order.

The Javadoc lists parameters in the wrong order.

 /**
  * Go through job records to find the matching payment using payment-id
  *
+ * @param context
  * @param payments    payment list fetched data from local DB
- * @param context
  */
app/src/org/commcare/fragments/connect/ConnectDeliveryDetailsFragment.java (1)

94-101: Consider extracting app installation check to a utility method

The app installation check logic could be reused elsewhere. Consider moving it to ConnectManager or a utility class.

-boolean installed = false;
-for (ApplicationRecord app : MultipleAppsUtil.appRecordArray()) {
-    if (job.getDeliveryAppInfo().getAppId().equals(app.getUniqueId())) {
-        installed = true;
-        break;
-    }
-}
-final boolean appInstalled = installed;
+final boolean appInstalled = ConnectManager.isAppInstalled(job.getDeliveryAppInfo().getAppId());
app/src/org/commcare/adapters/ConnectMessageAdapter.java (1)

41-47: Extract common bind logic to reduce code duplication

Both LeftViewHolder and RightViewHolder have identical bind methods. Consider extracting the common logic.

private static void bindMessage(TextView messageView, TextView userNameView, ConnectMessageChatData chat) {
    SpannableStringBuilder builder = new SpannableStringBuilder();
    builder.append(chat.getMessage());
    MarkupUtil.setMarkdown(messageView, builder, new SpannableStringBuilder());
    userNameView.setText(DateUtils.formatDateTime(chat.getTimestamp(), DateUtils.FORMAT_HUMAN_READABLE_SHORT));
}

// Then in LeftViewHolder:
public void bind(ConnectMessageChatData chat) {
    bindMessage(binding.tvChatMessage, binding.tvUserName, chat);
}

// And in RightViewHolder:
public void bind(ConnectMessageChatData chat) {
    bindMessage(binding.tvChatMessage, binding.tvUserName, chat);
}

Also applies to: 58-64

app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java (1)

79-79: Remove commented code

Commented code should be removed to improve code cleanliness. If needed, it can be retrieved from version control.

Remove the commented lines at:

  • Line 79: // updateUpdatedDate(job.getLastLearnUpdate());
  • Line 119: // updateUpdatedDate(new Date());
  • Lines 323-331: The entire commented updateUpdatedDate method

Also applies to: 119-119, 323-331

app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java (3)

315-317: Improve exception handling specificity.

The catch-all exception handling makes debugging difficult and may hide real issues.

-                } catch (Exception e) {
-                    //Ignore exception, happens if we leave the page before API call finishes
-                }
+                } catch (IllegalStateException e) {
+                    // Fragment not attached, safely ignore
+                    Logger.log("ConnectDeliveryProgress", "Fragment detached during update");
+                }

142-147: Performance concern: Inefficient view height calculation.

The code calls createFragment(position).getView() which may create a new fragment instance each time, and getView() could return null if the fragment's view hasn't been created yet. This could also cause performance issues if called frequently.

Consider using the existing fragment instances from the adapter's fragment list or implementing a more efficient height calculation mechanism:

-                    View view = viewStateAdapter.createFragment(position).getView();
-                    if(view != null) {
-                        pager.getLayoutParams().height = view.getMeasuredHeight();
-                        pager.requestLayout();
-                    }
+                    // Consider removing this dynamic height adjustment or using a different approach
+                    // as createFragment() may create new instances and getView() might be null

375-383: Type safety issue in ViewStateAdapter.refresh().

The instanceof checks are fragile and could break if fragment types change. Consider using interfaces or a more robust pattern.

Consider implementing a common interface for refreshable fragments:

+        public interface RefreshableFragment {
+            void updateView();
+        }
+
         public void refresh() {
             for (Fragment fragment : fragmentList) {
-                if (fragment instanceof ConnectDeliveryProgressDeliveryFragment) {
-                    ((ConnectDeliveryProgressDeliveryFragment) fragment).updateView();
-                } else if (fragment instanceof ConnectResultsSummaryListFragment) {
-                    ((ConnectResultsSummaryListFragment) fragment).updateView();
+                if (fragment instanceof RefreshableFragment) {
+                    ((RefreshableFragment) fragment).updateView();
                 }
             }
         }
app/src/org/commcare/fragments/connect/ConnectDeliveryProgressDeliveryFragment.java (1)

117-137: Use consistent Map implementations for better performance.

The code mixes HashMap and Hashtable usage. HashMap is generally preferred for better performance in single-threaded scenarios.

-            HashMap<String, HashMap<String, Integer>> paymentTypeAndStatusCounts = new HashMap<>();
+            Map<String, Map<String, Integer>> paymentTypeAndStatusCounts = new HashMap<>();

Also consider using Map.computeIfAbsent() to simplify the nested map initialization:

-                    if (!paymentTypeAndStatusCounts.containsKey(deliverySlug)) {
-                        paymentTypeAndStatusCounts.put(deliverySlug, new HashMap<>());
-                    }
+                    paymentTypeAndStatusCounts.computeIfAbsent(deliverySlug, k -> new HashMap<>());
app/src/org/commcare/fragments/connect/ConnectDeliveryListFragment.java (3)

165-169: Use more efficient RecyclerView update methods.

notifyDataSetChanged() causes the entire list to refresh. Consider using more specific notify methods for better performance.

 public void updateDeliveries(List<ConnectJobDeliveryRecord> newDeliveries) {
-    filteredDeliveries.clear();
-    filteredDeliveries.addAll(newDeliveries);
-    notifyDataSetChanged();
+    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
+        new DeliveryDiffCallback(filteredDeliveries, newDeliveries));
+    filteredDeliveries.clear();
+    filteredDeliveries.addAll(newDeliveries);
+    diffResult.dispatchUpdatesTo(this);
 }

This requires implementing a DiffUtil.Callback for better performance with large lists.


111-117: Modern switch expression is good, but consider using constants.

The switch expression is well-implemented, but the hardcoded resource IDs could be replaced with the existing constants for better maintainability.

     private int getFilterId(String filter) {
         return switch (filter) {
-            case APPROVED_IDENTIFIER -> R.id.llFilterApproved;
-            case REJECTED_IDENTIFIER -> R.id.llFilterRejected;
-            case PENDING_IDENTIFIER -> R.id.llFilterPending;
-            default -> R.id.cvFilterAll;
+            case APPROVED_IDENTIFIER -> R.id.llFilterApproved;
+            case REJECTED_IDENTIFIER -> R.id.llFilterRejected;
+            case PENDING_IDENTIFIER -> R.id.llFilterPending;
+            case ALL_IDENTIFIER, default -> R.id.cvFilterAll;
         };
     }

165-169: Inefficient RecyclerView update pattern.

Using notifyDataSetChanged() is inefficient as it rebuilds the entire list. Consider using more specific notify methods for better performance.

Consider using DiffUtil for more efficient updates:

         public void updateDeliveries(List<ConnectJobDeliveryRecord> newDeliveries) {
-            filteredDeliveries.clear();
-            filteredDeliveries.addAll(newDeliveries);
-            notifyDataSetChanged();
+            DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
+                new DeliveryDiffCallback(filteredDeliveries, newDeliveries));
+            filteredDeliveries.clear();
+            filteredDeliveries.addAll(newDeliveries);
+            diffResult.dispatchUpdatesTo(this);
         }
app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java (1)

61-65: Improve type safety for activity casting.

The instanceof check is good, but consider extracting the interface to avoid tight coupling to ConnectActivity.

 private void setBackButtonAndActionBarState(boolean enabled) {
     Activity activity = getActivity();
-    if(activity instanceof ConnectActivity connectActivity) {
-        connectActivity.setBackButtonAndActionBarState(enabled);
+    if (activity instanceof ConnectActivity) {
+        ((ConnectActivity) activity).setBackButtonAndActionBarState(enabled);
+    } else {
+        Logger.log("ConnectDownloading", "Activity is not ConnectActivity, cannot control back button");
     }
 }

Consider creating an interface for this functionality to reduce coupling.

app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (1)

95-97: Handle individual job parsing failures gracefully.

The current approach logs exceptions but continues processing. Consider whether partially failed job lists should be handled differently.

The current approach is reasonable, but consider tracking parsing success rate for monitoring:

                         for (int i = 0; i < json.length(); i++) {
                             try {
                                 JSONObject obj = (JSONObject) json.get(i);
                                 ConnectJobRecord job = ConnectJobRecord.fromJson(obj);
                                 jobs.add(job);
                             } catch (JSONException e) {
+                                Logger.log("ConnectUnlock", String.format("Failed to parse job %d of %d", i+1, json.length()));
                                 Logger.exception("Parsing return from Opportunities request", e);
                             }
                         }
app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java (1)

93-93: Incorrect comment references

Comments mention "payments" but the code handles channels and messages.

-//Delete payments that are no longer available
+//Delete channels that are no longer available
-//Delete payments that are no longer available
+//Delete messages that are no longer available

Also applies to: 167-167

app/src/org/commcare/models/connect/ConnectLoginJobListModel.java (1)

241-250: Incorrect class name in toString()

The toString() method references "AddListItem" instead of the actual class name.

 @Override
 public String toString() {
-    return "AddListItem{" +
+    return "ConnectLoginJobListModel{" +
             "name='" + name + '\'' +
             ", id=" + id +
             ", date=" + date +
             ", description='" + description + '\'' +
             ", organization='" + organization + '\'' +
             '}';
 }
app/src/org/commcare/fragments/connect/ConnectResultsSummaryListFragment.java (2)

121-122: Redundant variable assignment

The money variable is assigned but then a different calculation is used.

 String money = job.getMoneyString(Integer.parseInt(payment.getAmount()));
-paymentHolder.nameText.setText(job.getMoneyString(Integer.parseInt(payment.getAmount())));
+paymentHolder.nameText.setText(money);

263-273: Complex manual margin calculation

The dialog margin calculation could be simplified using resources.

Define margins in dimens.xml and use them:

<!-- In dimens.xml -->
<dimen name="dialog_horizontal_margin">10dp</dimen>
-int marginInDp = 10;
-float density = context.getResources().getDisplayMetrics().density;
-int marginInPx = (int) (marginInDp * density);
+int marginInPx = context.getResources().getDimensionPixelSize(R.dimen.dialog_horizontal_margin);
app/src/org/commcare/adapters/JobListConnectHomeAppsAdapter.java (1)

96-105: ```shell
#!/bin/bash

Locate the declaration of the shared DateFormat and its pattern

rg -n "static.*dateFormat" app/src/org/commcare/connect/ConnectManager.java

Print the ConnectManager.formatDate method with its context

sed -n '100,150p' app/src/org/commcare/connect/ConnectManager.java


</blockquote></details>
<details>
<summary>app/src/org/commcare/connect/MessageManager.java (1)</summary><blockquote>

`72-151`: **Consider extracting common callback pattern**

The API callback implementations follow a similar pattern across multiple methods. Consider creating a base callback class to reduce code duplication.


Would you like me to generate a base callback class that handles common error scenarios to reduce the repetitive callback implementations?

</blockquote></details>
<details>
<summary>app/src/org/commcare/connect/ConnectManager.java (3)</summary><blockquote>

`411-444`: **Refactor duplicated error handling code**

The error handling code is duplicated across multiple API callback implementations. This violates the DRY principle.

Consider creating a base callback class or utility methods to handle common error scenarios:

```java
public abstract class BaseConnectApiCallback implements IApiCallback {
    protected final Context context;
    protected final ConnectActivityCompleteListener listener;
    
    protected BaseConnectApiCallback(Context context, ConnectActivityCompleteListener listener) {
        this.context = context;
        this.listener = listener;
    }
    
    protected abstract void reportApiCall(boolean success);
    
    @Override
    public void processFailure(int responseCode, @Nullable InputStream errorResponse) {
        Logger.log("ERROR", String.format(Locale.getDefault(), "Failed: %d", responseCode));
        reportApiCall(false);
        listener.connectActivityComplete(false);
    }
    
    @Override
    public void processNetworkFailure() {
        Logger.log("ERROR", "Failed (network)");
        reportApiCall(false);
        listener.connectActivityComplete(false);
    }
    
    // ... other common error handlers
}

Also applies to: 538-569, 590-623


174-174: Fix formatting: Add space after 'if'

-        if(record == null || activeJob == null) {
+        if (record == null || activeJob == null) {

142-146: Method has unexpected side effects

The method wasAppLaunchedFromConnect() clears primedAppIdForAutoLogin as a side effect, which could be problematic if called multiple times or for checking purposes only.

Consider separating the check from the clearing operation:

 public static boolean wasAppLaunchedFromConnect(String appId) {
     String primed = primedAppIdForAutoLogin;
-    primedAppIdForAutoLogin = null;
     return primed != null && primed.equals(appId);
 }
+
+public static void clearPrimedAppId() {
+    primedAppIdForAutoLogin = null;
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9acd338 and 5cfcd90.

⛔ Files ignored due to path filters (3)
  • app/res/drawable/icon_chevron_left_brand.png is excluded by !**/*.png
  • app/res/drawable/icon_chevron_left_primary.png is excluded by !**/*.png
  • app/res/font/roboto_medium.ttf is excluded by !**/*.ttf
📒 Files selected for processing (84)
  • app/AndroidManifest.xml (7 hunks)
  • app/build.gradle (2 hunks)
  • app/res/drawable-night/ic_connect_message_large.xml (1 hunks)
  • app/res/drawable/ic_connect_payment_revert.xml (0 hunks)
  • app/res/layout-land/home_screen.xml (0 hunks)
  • app/res/layout/fragment_connect_delivery_details.xml (2 hunks)
  • app/res/layout/fragment_connect_delivery_progress.xml (1 hunks)
  • app/res/layout/fragment_connect_job_intro.xml (1 hunks)
  • app/res/layout/fragment_connect_jobs_list.xml (2 hunks)
  • app/res/layout/fragment_connect_learning_progress.xml (1 hunks)
  • app/res/layout/fragment_connect_progress_delivery.xml (1 hunks)
  • app/res/layout/item_channel.xml (1 hunks)
  • app/res/layout/screen_personalid_phone_verify.xml (1 hunks)
  • app/res/menu/menu_connect.xml (1 hunks)
  • app/res/values-fr/strings.xml (1 hunks)
  • app/res/values-pt/strings.xml (1 hunks)
  • app/res/values-sw/strings.xml (1 hunks)
  • app/res/values-ti/strings.xml (1 hunks)
  • app/res/values/dimens.xml (1 hunks)
  • app/res/values/strings.xml (1 hunks)
  • app/src/org/commcare/CommCareApplication.java (4 hunks)
  • app/src/org/commcare/CommCareNoficationManager.java (3 hunks)
  • app/src/org/commcare/activities/LoginActivity.java (3 hunks)
  • app/src/org/commcare/activities/LoginActivityUIController.java (1 hunks)
  • app/src/org/commcare/activities/StandardHomeActivity.java (6 hunks)
  • app/src/org/commcare/activities/StandardHomeActivityUIController.java (4 hunks)
  • app/src/org/commcare/activities/connect/ConnectActivity.java (1 hunks)
  • app/src/org/commcare/activities/connect/ConnectMessagingActivity.java (1 hunks)
  • app/src/org/commcare/activities/connect/PersonalIdActivity.java (0 hunks)
  • app/src/org/commcare/adapters/ChannelAdapter.java (1 hunks)
  • app/src/org/commcare/adapters/ConnectDeliveryProgressReportAdapter.java (1 hunks)
  • app/src/org/commcare/adapters/ConnectMessageAdapter.java (1 hunks)
  • app/src/org/commcare/adapters/ConnectProgressJobSummaryAdapter.java (1 hunks)
  • app/src/org/commcare/adapters/JobListConnectHomeAppsAdapter.java (1 hunks)
  • app/src/org/commcare/android/database/connect/models/ConnectJobAssessmentRecord.java (1 hunks)
  • app/src/org/commcare/android/database/connect/models/ConnectJobLearningRecord.java (1 hunks)
  • app/src/org/commcare/connect/ConnectConstants.java (1 hunks)
  • app/src/org/commcare/connect/ConnectJobListLauncher.java (1 hunks)
  • app/src/org/commcare/connect/ConnectManager.java (1 hunks)
  • app/src/org/commcare/connect/IConnectAppLauncher.java (1 hunks)
  • app/src/org/commcare/connect/MessageManager.java (1 hunks)
  • app/src/org/commcare/connect/PersonalIdManager.java (5 hunks)
  • app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (3 hunks)
  • app/src/org/commcare/connect/database/ConnectDatabaseUtils.java (4 hunks)
  • app/src/org/commcare/connect/database/ConnectJobUtils.java (2 hunks)
  • app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java (1 hunks)
  • app/src/org/commcare/connect/database/ConnectUserDatabaseUtil.java (2 hunks)
  • app/src/org/commcare/connect/database/JobStoreManager.java (2 hunks)
  • app/src/org/commcare/connect/network/ApiPersonalId.java (7 hunks)
  • app/src/org/commcare/connect/network/ConnectSsoHelper.java (1 hunks)
  • app/src/org/commcare/connect/network/PersonalIdApiHandler.java (1 hunks)
  • app/src/org/commcare/connect/network/parser/ConfirmBackupCodeResponseParser.java (1 hunks)
  • app/src/org/commcare/fragments/SelectInstallModeFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectDeliveryDetailsFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectDeliveryListFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectDeliveryProgressDeliveryFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectJobDetailBottomSheetDialogFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectJobsListsFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectResultsSummaryListFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelConsentBottomSheet.java (1 hunks)
  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelListFragment.java (1 hunks)
  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageChatData.java (1 hunks)
  • app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1 hunks)
  • app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java (2 hunks)
  • app/src/org/commcare/fragments/personalId/PersonalIdMessageFragment.java (1 hunks)
  • app/src/org/commcare/interfaces/OnJobSelectionClick.java (1 hunks)
  • app/src/org/commcare/models/connect/ConnectLoginJobListModel.java (1 hunks)
  • app/src/org/commcare/network/HttpCalloutTask.java (1 hunks)
  • app/src/org/commcare/services/CommCareFirebaseMessagingService.java (2 hunks)
  • app/src/org/commcare/services/PaymentAcknowledgeReceiver.java (1 hunks)
  • app/src/org/commcare/tasks/ManageKeyRecordTask.java (1 hunks)
  • app/src/org/commcare/utils/BiometricsHelper.java (3 hunks)
  • app/src/org/commcare/utils/CommCareNavigationController.kt (1 hunks)
  • app/src/org/commcare/views/connect/CustomOtpView.java (1 hunks)
  • app/src/org/commcare/views/connect/LinearProgressBar.java (0 hunks)
  • app/unit-tests/src/org/commcare/CommCareTestApplication.java (5 hunks)
  • app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java (4 hunks)
  • commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/CommCareLauncher.java (1 hunks)
💤 Files with no reviewable changes (4)
  • app/src/org/commcare/activities/connect/PersonalIdActivity.java
  • app/res/layout-land/home_screen.xml
  • app/res/drawable/ic_connect_payment_revert.xml
  • app/src/org/commcare/views/connect/LinearProgressBar.java
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
app/src/org/commcare/connect/network/PersonalIdApiHandler.java (1)
Learnt from: OrangeAndGreen
PR: dimagi/commcare-android#3108
File: app/src/org/commcare/connect/network/PersonalIdApiHandler.java:51-56
Timestamp: 2025-06-13T15:53:12.951Z
Learning: For `PersonalIdApiHandler`, the team’s convention is to propagate `JSONException` as an unchecked `RuntimeException` so the app crashes, signalling a contract/implementation bug rather than attempting a graceful retry.
app/src/org/commcare/activities/StandardHomeActivity.java (1)
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
app/src/org/commcare/activities/connect/ConnectMessagingActivity.java (1)
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1)
Learnt from: pm-dimagi
PR: dimagi/commcare-android#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.
app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (1)
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.
app/src/org/commcare/activities/connect/ConnectActivity.java (1)
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
app/src/org/commcare/models/connect/ConnectLoginJobListModel.java (1)
Learnt from: OrangeAndGreen
PR: dimagi/commcare-android#3108
File: app/src/org/commcare/models/connect/ConnectLoginJobListModel.java:79-92
Timestamp: 2025-06-20T15:51:14.138Z
Learning: The ConnectLoginJobListModel class in app/src/org/commcare/models/connect/ConnectLoginJobListModel.java does not need to implement Parcelable interface as it is not passed between Android activities or fragments.
app/src/org/commcare/connect/ConnectManager.java (1)
Learnt from: shubham1g5
PR: dimagi/commcare-android#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.
🔇 Additional comments (103)
app/res/layout/item_channel.xml (1)

70-72: No action required – whitespace-only change
The added blank line does not affect rendering or constraints. Safe to keep or drop as preferred.

app/res/values/strings.xml (1)

611-614: Verify locale parity and runtime usage for the new lock-out strings
personalid_recovery_lockout_title / _message look good, but please ensure:

  1. Equivalent entries exist in every values-XX/strings.xml, else users on those locales will see fallback English.
  2. All code paths reference these exact IDs (case-sensitive) to avoid “resource not found” crashes.

Run once from repo root:

#!/bin/bash
# 1. Check other locale files contain the new keys
echo "== Locale coverage =="
rg -n 'personalid_recovery_lockout_title' app/res/values*/*.xml || true

# 2. Check Java/Kotlin/Xml layout usage
echo "== Code usage =="
rg -n 'R.string.personalid_recovery_lockout_' app/src || true

The first block should list an entry for every values-xx folder you maintain; the second should show at least one usage site (e.g., in PersonalIdBackupCodeFragment).

app/res/drawable-night/ic_connect_message_large.xml (1)

1-18: Night-mode icon added – double-check day-mode counterpart & size
The vector is accepted; however:
• Only a night-mode version is present. Verify that a default (day) variant exists so the icon doesn’t disappear in light theme.
• The single path contains 1 400+ commands; consider running vector-drawable-optimize to reduce APK size if this asset ships untouched from Figma.

app/res/layout/screen_personalid_phone_verify.xml (1)

158-158: Whitespace-only newline – no functional impact.

app/res/values/dimens.xml (1)

154-154: Whitespace-only change – safe to ignore.

app/src/org/commcare/views/connect/CustomOtpView.java (1)

103-103: LGTM: Minor formatting improvement.

The removal of extra space in the cast expression improves code readability without affecting functionality.

app/src/org/commcare/activities/LoginActivityUIController.java (1)

565-573: Verify the connect button visibility logic change.

The connect button is now shown when the user is logged in via PersonalIdManager, which is the opposite of the previous behavior. Please confirm this change aligns with the intended user experience for the Connect feature.

#!/bin/bash
# Description: Search for related Connect button usage and visibility logic to understand the intended behavior

# Search for other references to setConnectButtonVisible to understand usage patterns
rg -A 3 -B 3 "setConnectButtonVisible"

# Search for Connect button related logic and comments
rg -A 5 -B 5 "connect.*button.*visible|connectLoginButton.*setVisibility"

# Look for any documentation or comments about Connect button behavior when logged in
rg -A 5 -B 5 "logged.*in.*connect|connect.*logged.*in"
app/src/org/commcare/connect/ConnectConstants.java (1)

54-54: LGTM: Appropriate constant addition for account lockout handling.

The new PERSONALID_RECOVERY_ACCOUNT_LOCKED constant follows the established pattern and directly supports the PR objective of handling user lockout scenarios during backup code recovery.

app/res/layout/fragment_connect_delivery_progress.xml (1)

76-76: LGTM: Improved button responsiveness.

Changing the button height from fixed 35dp to wrap_content makes the payment confirmation buttons more responsive to content and system font settings, improving accessibility and responsive design.

Also applies to: 86-86

app/res/layout/fragment_connect_progress_delivery.xml (1)

163-163: LGTM: Better vertical layout flow.

Moving the sync button below the descriptive text (instead of top-aligning with it) creates a more logical visual hierarchy and improves the overall layout flow.

app/res/layout/fragment_connect_jobs_list.xml (1)

42-42: Layout constraint updates look correct.

The constraint changes properly reposition the UI elements after removing the alert tile, maintaining logical layout flow by anchoring to connect_jobs_last_update.

Also applies to: 51-51

app/src/org/commcare/utils/BiometricsHelper.java (2)

42-42: Good refactoring to centralize the biometric constant.

Introducing the StrongBiometric constant improves maintainability and consistency across the class.


52-52: Consistent usage of the new constant.

All references to BiometricManager.Authenticators.BIOMETRIC_STRONG have been properly replaced with the centralized constant.

Also applies to: 73-73, 202-202

app/res/layout/fragment_connect_learning_progress.xml (1)

175-175: Layout constraint improvement.

Explicitly positioning the sync button relative to the text view improves layout clarity and maintains proper UI hierarchy.

app/build.gradle (2)

93-99: LGTM - Good dependency reorganization.

Moving the Google Play Services dependencies earlier in the block alongside other core dependencies improves the organization and maintainability of the build file.


628-628: Minor typo fix appreciated.

Good catch fixing the comment from "skip non-digit" to "skip non-numeric" for better clarity.

app/src/org/commcare/connect/network/PersonalIdApiHandler.java (1)

97-103: LGTM - Proper exception handling separation.

The separation of JSONException and IOException handling aligns with your team's established convention. JSONException being escalated as a RuntimeException correctly signals a contract/implementation bug that should crash the app, while IOException continues to be handled gracefully as a recoverable network issue.

app/res/values-pt/strings.xml (1)

459-461: LGTM - Appropriate Portuguese translations for PersonalID lockout.

The Portuguese translations correctly convey the account lockout scenario described in the PR objectives, supporting users who have entered incorrect backup codes too many times.

app/src/org/commcare/CommCareNoficationManager.java (3)

25-28: LGTM - Necessary imports for notification enhancements.

The additional imports support the notification channel enhancements for Connect messaging functionality.


43-43: LGTM - Consistent naming convention.

The new messaging notification channel ID follows the established naming pattern used by other notification channels in this class.


200-203: LGTM - Appropriate notification channel for Connect messaging.

The new messaging notification channel with maximum importance is well-suited for Connect messaging functionality, ensuring users receive timely notifications about important messages.

app/res/menu/menu_connect.xml (1)

1-16: LGTM - Well-structured Connect menu.

The menu provides appropriate notification and sync actions for the Connect feature, with sensible icon choices and proper action bar placement using showAsAction="always".

app/src/org/commcare/interfaces/OnJobSelectionClick.java (1)

5-7: LGTM! Clean interface design.

The interface follows good design principles with a clear, focused responsibility for handling job selection events. The parameter list provides comprehensive context needed for job selection handling.

app/src/org/commcare/network/HttpCalloutTask.java (1)

149-149: LGTM! Proper exception declaration.

The method signature correctly declares the token-related exceptions that implementations may throw. The existing exception handling in the doTaskBackground method (lines 105-111) already properly catches and handles these exceptions.

commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/CommCareLauncher.java (1)

16-16: LGTM! Appropriate intent flags for app launching.

The addition of FLAG_ACTIVITY_CLEAR_TOP and FLAG_ACTIVITY_NEW_TASK flags ensures proper activity stack management when launching CommCare apps from external contexts, which is essential for the Connect integration.

app/src/org/commcare/connect/database/ConnectJobUtils.java (1)

62-64: LGTM! Clean delegation pattern.

The new storeJobs method provides a clean delegation to JobStoreManager.storeJobs while maintaining the same interface.

app/res/values-ti/strings.xml (1)

445-447: LGTM! Strings support PersonalID lockout feature.

These new Tigrinya localization strings directly support the PR objective of handling PersonalID lockout after multiple failed backup code attempts. The strings provide appropriate user messaging for the lockout scenario:

  • Account locked title
  • Explanation that the account was locked due to multiple incorrect backup code entries

This aligns perfectly with the "PersonalID Lockout" feature described in the PR objectives.

app/res/values-fr/strings.xml (1)

453-455: LGTM! French localization strings are accurate.

The French translations for the PersonalID lockout scenario are correctly implemented and linguistically appropriate. The strings properly convey the account lockout message to French-speaking users.

app/src/org/commcare/connect/ConnectJobListLauncher.java (1)

7-9: LGTM! Clean interface design.

The interface provides a clear contract for launching Connect jobs with appropriate parameters. The method signature makes sense for the intended functionality.

app/src/org/commcare/connect/database/JobStoreManager.java (2)

39-39: Minor formatting fix.

Removed unnecessary space in catch statement for consistency.


99-122: ```shell
#!/bin/bash

Extract key sections of JobStoreManager to inspect exception handling

sed -n '1,50p;80,140p' app/src/org/commcare/connect/database/JobStoreManager.java


</details>
<details>
<summary>app/res/layout/fragment_connect_job_intro.xml (3)</summary>

`76-76`: **Good UI improvement.**

Changing to vertical orientation provides better layout flexibility for the text elements.

---

`79-93`: **Better text element organization.**

Moving the TextViews as direct siblings with proper margins improves the layout structure and readability.

---

`99-99`: **Follows Material Design guidelines.**

End alignment for action buttons is consistent with Material Design patterns and provides better visual hierarchy.

</details>
<details>
<summary>app/src/org/commcare/fragments/personalId/PersonalIdMessageFragment.java (1)</summary>

`139-141`: **Correctly implements PersonalID lockout handling.**

The addition of the `PERSONALID_RECOVERY_ACCOUNT_LOCKED` case properly handles the lockout scenario by finishing the activity, which aligns with the PR objectives. The code simplification by removing explicit casting is also good.

</details>
<details>
<summary>app/src/org/commcare/tasks/ManageKeyRecordTask.java (1)</summary>

`349-349`: **LGTM: Proper exception declaration for enhanced error handling.**

The method signature correctly declares the additional checked exceptions that can be propagated during HTTP requests, supporting the enhanced token-related error handling in Connect network operations.

</details>
<details>
<summary>app/src/org/commcare/connect/database/ConnectDatabaseHelper.java (2)</summary>

`43-51`: **LGTM: Improved database rekeying logic with analytics.**

The refined logic correctly rekeys the database only when necessary (database is open AND passphrases differ) and adds valuable Firebase analytics reporting. Storing the remote passphrase regardless of rekeying ensures consistency.

---

`84-85`: **Good addition: Legacy usage tracking with encryption context.**

The logging provides valuable insights into legacy database access patterns and includes helpful context about whether the passphrase is encrypted or unencrypted.

</details>
<details>
<summary>app/res/values-sw/strings.xml (1)</summary>

`459-462`: **LGTM: Proper localization for PersonalID account lockout.**

The Swahili translations correctly support the new account lockout scenario, with appropriate title and message explaining the lockout due to multiple incorrect backup code attempts.

</details>
<details>
<summary>app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java (2)</summary>

`205-207`: **LGTM: Proper account lockout detection logic.**

The condition correctly identifies the "LOCKED_ACCOUNT" session failure code and routes to the dedicated lockout handler, implementing the PR's core requirement for handling multiple failed backup code attempts.

---

`255-261`: **LGTM: Well-structured account lockout handler.**

The `handleAccountLockout()` method follows the existing pattern of logging the result, clearing input fields, and navigating to an appropriate message screen using the correct constants and localized strings.

</details>
<details>
<summary>app/src/org/commcare/fragments/SelectInstallModeFragment.java (1)</summary>

`214-214`: **LGTM: Proper access control for connect button.**

The updated condition correctly enables the connect button only when both the feature is enabled and the user is logged in via PersonalIdManager, preventing unauthorized interactions.

</details>
<details>
<summary>app/src/org/commcare/android/database/connect/models/ConnectJobAssessmentRecord.java (1)</summary>

`55-55`: ```bash
#!/bin/bash
# Locate the DateUtils.java file in the repo
file=$(fd --glob "DateUtils.java" | head -n1)
if [ -z "$file" ]; then
  echo "No DateUtils.java found"
  exit 0
fi

echo "Inspecting $file"
# Show the method signature and throws clause
grep -n "parseDateTime" "$file"
grep -n "throws .*ParseException" "$file"
app/src/org/commcare/connect/network/ConnectSsoHelper.java (1)

117-118: LGTM: Improved code readability.

The formatting change splitting the throws clause across multiple lines enhances readability without affecting functionality.

app/unit-tests/src/org/commcare/CommCareTestApplication.java (2)

91-98: LGTM: Proper WorkManager test initialization.

Configuring WorkManager with SynchronousExecutor ensures deterministic test execution by making background tasks run synchronously during tests.


285-290: LGTM: Ensures test isolation.

The cleanup of CommCareApp sandbox and ConnectDatabaseHelper state after each test provides proper isolation, preventing test interference.

app/src/org/commcare/activities/LoginActivity.java (2)

412-414: LGTM: Connect-managed password override integration.

The addition of ConnectManager.checkAutoLoginAndOverridePassword provides Connect-specific auto-login functionality, allowing Connect to manage user credentials seamlessly.


147-147: ```bash
#!/bin/bash

Search for the definition of wasAppLaunchedFromConnect in ConnectManager.java

rg -n "wasAppLaunchedFromConnect" -g "*ConnectManager.java"

Search for the old method definition in PersonalIdManager.java for signature comparison

rg -n "wasAppLaunchedFromConnect" -g "*PersonalIdManager.java"


</details>
<details>
<summary>app/src/org/commcare/connect/database/ConnectDatabaseUtils.java (2)</summary>

`8-8`: **Good improvement in exception handling specificity.**

The changes correctly narrow exception handling from generic `Exception` to specific `EncryptionUtils.EncryptionException` and `Base64DecoderException`. This improves code clarity and allows for more precise error handling.



Also applies to: 40-42, 57-59, 89-91

---

`40-42`: **Excellent improvement in exception handling specificity.**

The change from generic `Exception` to specific exception types (`EncryptionUtils.EncryptionException` and `Base64DecoderException`) provides better error handling granularity and allows callers to handle different failure modes appropriately. This aligns with best practices for exception handling.




Also applies to: 57-59, 89-91

</details>
<details>
<summary>app/src/org/commcare/connect/database/ConnectUserDatabaseUtil.java (2)</summary>

`16-25`: **Good simplification of exception handling.**

Removing internal try-catch blocks allows exceptions to propagate naturally to higher-level components that can handle them more appropriately. This aligns well with the exception handling refinements in related database utility classes.



Also applies to: 34-34, 38-42

---

`16-25`: **Good simplification of exception handling.**

Removing the try-catch blocks allows exceptions to propagate naturally to callers who can handle them more appropriately. This aligns with the exception handling improvements seen across the Connect module and follows best practices by not swallowing exceptions at the database utility level.




Also applies to: 34-34, 37-42

</details>
<details>
<summary>app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java (2)</summary>

`4-14`: **Good organizational improvements.**

The import reorganization and class list formatting enhance code readability and maintainability. The variable name update in the test method also improves clarity.



Also applies to: 51-398, 409-409

---

`4-14`: **Good maintenance and organization improvements.**

The import reorganization and externalizable class list maintenance help keep the test code clean and up-to-date. These changes support the ongoing development of the Connect module.




Also applies to: 377-377, 409-414

</details>
<details>
<summary>app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelConsentBottomSheet.java (1)</summary>

`25-58`: **Well-structured consent UI implementation.**

The bottom sheet effectively handles channel consent with proper navigation flow on acceptance. The use of Navigation Components and proper error handling with Toast messages demonstrates good Android development practices.

</details>
<details>
<summary>app/src/org/commcare/adapters/ChannelAdapter.java (1)</summary>

`24-53`: **Well-structured RecyclerView adapter implementation.**

The adapter follows standard Android patterns with proper ViewHolder implementation and click handling through interfaces. The separation of concerns and binding logic is well organized.

</details>
<details>
<summary>app/src/org/commcare/utils/CommCareNavigationController.kt (2)</summary>

`9-21`: **Excellent defensive navigation implementation.**

The `navigateSafely` extension function implements comprehensive safety checks to prevent navigation crashes, including validation of navigation actions and destination existence in the graph. This is particularly valuable for complex navigation flows in the Connect module.

---

`23-25`: **Clean and consistent utility extension.**

The `orEmpty` extension provides a clean way to handle nullable destination IDs with a sensible default. The naming is consistent with Kotlin's standard library conventions.

</details>
<details>
<summary>app/src/org/commcare/activities/StandardHomeActivity.java (1)</summary>

`113-113`: **Good integration point for Connect progress updates.**

Calling `updateConnectJobProgress()` before sync operations ensures job progress is current before data synchronization begins. This integration follows the app's existing patterns.

</details>
<details>
<summary>app/src/org/commcare/fragments/connect/ConnectJobDetailBottomSheetDialogFragment.java (2)</summary>

`70-83`: **Well-structured payment text building logic.**

The `buildPaymentText()` method cleanly handles both multi-payment and single-payment scenarios with appropriate formatting. The use of StringBuilder for concatenation is efficient.

---

`85-89`: **Proper view binding lifecycle management.**

The fragment correctly nullifies the binding in `onDestroyView()` to prevent memory leaks, following Android best practices.

</details>
<details>
<summary>app/src/org/commcare/activities/connect/ConnectMessagingActivity.java (2)</summary>

`31-43`: **Excellent navigation and analytics setup.**

The navigation controller setup with Firebase Analytics integration and proper action bar configuration follows Android best practices. The intent handling provides flexible entry points into the messaging flow.

---

`51-63`: **Proper lifecycle management for navigation listener.**

The `onDestroy()` method correctly removes the destination listener to prevent memory leaks, with appropriate null checks for robustness.

</details>
<details>
<summary>app/res/layout/fragment_connect_delivery_details.xml (1)</summary>

`163-208`: **Layout simplification looks good, but verify button constraints.**

The change from ConstraintLayout to LinearLayout simplifies the layout hierarchy, which is beneficial for performance. However, I notice some potential issues:

1. The LinearLayout has constraint attributes (lines 168-171) which are unnecessary since it's now a child of ConstraintLayout but using LinearLayout internally
2. The button positioning should work correctly with `android:layout_gravity="end"`



Consider cleaning up the unused constraint attributes on the LinearLayout:

```diff
 <LinearLayout
     android:id="@+id/linearLayout"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical"
-    app:layout_constraintTop_toTopOf="parent"
-    app:layout_constraintBottom_toBottomOf="parent"
-    app:layout_constraintStart_toStartOf="parent"
-    android:layout_marginEnd="2dp"
     android:background="@color/connect_darker_blue_color">
app/src/org/commcare/connect/PersonalIdManager.java (3)

455-466: Verify ConnectActivity is properly implemented before uncommenting navigation.

The navigation to ConnectActivity has been uncommented, which suggests this activity is now available. Ensure the activity is properly implemented and can handle the expected intents.

Run this script to verify ConnectActivity implementation:

#!/bin/bash
# Check if ConnectActivity is properly implemented
fd ConnectActivity.java --exec cat {}

# Check for any issues with intent handling
ast-grep --pattern 'class ConnectActivity {
  $$$
  onCreate($$$) {
    $$$
  }
  $$$
}'

128-128: Verify CrashUtil.registerUserData() placement and usage.

Adding CrashUtil.registerUserData() call in the init method appears to be for crash reporting integration. Ensure this call is appropriate at this point in the initialization flow and doesn't cause performance issues.

Run this script to check CrashUtil usage patterns:

#!/bin/bash
# Check other places where CrashUtil.registerUserData is called
rg "CrashUtil\.registerUserData" --type java -A 2 -B 2

# Check if there are any threading or performance considerations
ast-grep --pattern 'CrashUtil.registerUserData($$$)'

438-450: Let’s locate and inspect the ConnectActivityCompleteListener declaration to ensure it’s a single-method (functional) interface:

#!/bin/bash
# Locate and inspect the ConnectActivityCompleteListener interface
rg -A 5 "interface ConnectActivityCompleteListener" --type java
app/AndroidManifest.xml (2)

191-192: Inconsistency with PR objectives detected.

The manifest changes add ConnectActivity and other Connect-related components, but the PR objectives state this is about "PersonalID Lockout" for handling backup code failures. The actual changes appear to implement a comprehensive Connect module rather than addressing lockout functionality.

Please verify that:

  1. These Connect module additions are intended for this PR
  2. The PersonalID lockout functionality is included in these changes
  3. The PR description accurately reflects the scope of changes

Likely an incorrect or invalid review comment.


600-600: Verify PaymentAcknowledgeReceiver implementation and necessity.

A new PaymentAcknowledgeReceiver has been added without intent filters or permissions. Ensure this receiver is properly implemented and consider if it needs specific intent filters for payment acknowledgment actions.

Run this script to verify the receiver implementation:

#!/bin/bash
# Check if PaymentAcknowledgeReceiver is implemented
fd PaymentAcknowledgeReceiver.java --exec cat {}

# Check for any references to payment acknowledgment
rg "PaymentAcknowledge" --type java -A 3 -B 3
app/src/org/commcare/CommCareApplication.java (2)

386-389: Good placement of crash data registration.

Calling CrashUtil.registerUserData() after switching logger storage to global ensures user-specific crash data is properly cleared during session closure.


436-444: Properly implemented Connect analytics integration.

The null checks before setting Connect user properties ensure safe execution, and the analytics enhancement will help track Connect feature usage.

app/src/org/commcare/fragments/connectMessaging/ConnectMessageFragment.java (1)

101-108: Good error handling implementation

The error handling in the message retrieval callback is well implemented with appropriate user feedback.

app/src/org/commcare/adapters/ConnectDeliveryProgressReportAdapter.java (2)

38-38: Clarify the need for @SuppressLint annotation

The @SuppressLint("SetTextI18n") annotation suggests there might be hardcoded string concatenation. However, I don't see any direct string concatenation in the method. Consider removing this annotation if it's not needed.

If the lint warning is coming from the String.valueOf() calls, consider using proper string formatting or resources instead of suppressing the warning.


52-62: Well-implemented switch expression for dynamic text

The switch expression elegantly handles different cases for remaining days with proper pluralization and edge cases.

app/src/org/commcare/fragments/connect/ConnectDeliveryProgressDeliveryFragment.java (3)

72-75: Add null safety check for active job.

Similar to other fragments, this method uses ConnectManager.getActiveJob() without null checks, creating potential NPE risks.

 public void updateView() {
     ConnectJobRecord job = ConnectManager.getActiveJob();
-    if (job == null || view == null) {
+    if (job == null) {
+        Logger.log("ConnectDelivery", "No active job available for progress update");
         return;
     }
+    if (view == null) {
+        return;
+    }

Likely an incorrect or invalid review comment.


77-79: Potential division by zero.

While there's a check for total > 0, the calculation 100 * completed / total could still result in integer division issues and potential overflow with large values.

-        int percent = total > 0 ? (100 * completed / total) : 100;
+        int percent = total > 0 ? Math.min(100, (100 * completed) / total) : 100;

Likely an incorrect or invalid review comment.


156-157: ```shell
#!/bin/bash

Locate and display the implementation of getDaysRemaining in ConnectJobRecord

rg -n -C5 "getDaysRemaining" app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java


</details>
<details>
<summary>app/src/org/commcare/fragments/connect/ConnectDeliveryListFragment.java (1)</summary>

`111-117`: **Verify Java version compatibility for switch expressions.**

Switch expressions are a Java 14+ feature. Ensure the project's target Java version supports this syntax.



```shell
#!/bin/bash
# Check the project's Java target version
find . -name "*.gradle*" -exec grep -H "sourceCompatibility\|targetCompatibility\|JavaVersion" {} \;

If Java 14+ is not supported, use traditional switch statement:

-    private int getFilterId(String filter) {
-        return switch (filter) {
-            case APPROVED_IDENTIFIER -> R.id.llFilterApproved;
-            case REJECTED_IDENTIFIER -> R.id.llFilterRejected;
-            case PENDING_IDENTIFIER -> R.id.llFilterPending;
-            default -> R.id.cvFilterAll;
-        };
-    }
+    private int getFilterId(String filter) {
+        switch (filter) {
+            case APPROVED_IDENTIFIER:
+                return R.id.llFilterApproved;
+            case REJECTED_IDENTIFIER:
+                return R.id.llFilterRejected;
+            case PENDING_IDENTIFIER:
+                return R.id.llFilterPending;
+            default:
+                return R.id.cvFilterAll;
+        }
+    }
app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java (3)

74-78: Add null safety checks for job and app record access.

The method accesses job data without null checks, which could cause NPEs during app download initiation.

 private void startAppDownload() {
     ConnectJobRecord job = ConnectManager.getActiveJob();
+    if (job == null) {
+        Logger.log("ConnectDownloading", "No active job for app download");
+        requireActivity().finish();
+        return;
+    }
     ConnectAppRecord record = getLearnApp ? job.getLearnAppInfo() : job.getDeliveryAppInfo();
+    if (record == null) {
+        Logger.log("ConnectDownloading", "No app info available for download");
+        showInstallFailError(AppInstallStatus.UnknownFailure);
+        return;
+    }
     ConnectManager.downloadAppOrResumeUpdates(record.getInstallUrl(), this);
 }
⛔ Skipped due to learnings
Learnt from: OrangeAndGreen
PR: dimagi/commcare-android#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.

67-72: Duplicate null check missing.

Same issue as the previous method - missing null check for getActivity().

     private void setWaitDialogEnabled(boolean enabled) {
         Activity activity = getActivity();
+        if (activity == null) {
+            return;
+        }
         if(activity instanceof ConnectActivity connectActivity) {
             connectActivity.setWaitDialogEnabled(enabled);
         }
     }

Likely an incorrect or invalid review comment.


61-65: Good use of pattern matching for instanceof, but missing null check.

The pattern matching with instanceof is modern and clean, but there's no null check for getActivity() which could return null.

     private void setBackButtonAndActionBarState(boolean enabled) {
         Activity activity = getActivity();
+        if (activity == null) {
+            return;
+        }
         if(activity instanceof ConnectActivity connectActivity) {
             connectActivity.setBackButtonAndActionBarState(enabled);
         }
     }

Likely an incorrect or invalid review comment.

app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java (1)

1-171: File unrelated to PR objectives

This file implements Connect job results functionality, which is unrelated to the stated PR objectives about PersonalID lockout and backup code handling. The PR description indicates this should handle server error_code responses when users fail backup code entry, but this file deals with Connect job payments and deliveries instead.

Likely an incorrect or invalid review comment.

app/src/org/commcare/activities/connect/ConnectActivity.java (2)

1-251: File unrelated to PR objectives

This activity manages the Connect feature UI and navigation, which doesn't align with the PR's stated purpose of handling PersonalID lockout and backup code failure scenarios.

Likely an incorrect or invalid review comment.


108-111: Potential duplicate receiver registration

The broadcast receiver could be registered multiple times if onResume() is called without onPause().

Track registration state:

+private boolean isReceiverRegistered = false;

 @Override
 protected void onResume() {
     super.onResume();
     updateMessagingIcon();
 
-    LocalBroadcastManager.getInstance(this).registerReceiver(updateReceiver,
-            new IntentFilter(CommCareFirebaseMessagingService.MESSAGING_UPDATE_BROADCAST));
+    if (!isReceiverRegistered) {
+        LocalBroadcastManager.getInstance(this).registerReceiver(updateReceiver,
+                new IntentFilter(CommCareFirebaseMessagingService.MESSAGING_UPDATE_BROADCAST));
+        isReceiverRegistered = true;
+    }
 }

 @Override
 public void onPause() {
     super.onPause();
-    LocalBroadcastManager.getInstance(this).unregisterReceiver(updateReceiver);
+    if (isReceiverRegistered) {
+        LocalBroadcastManager.getInstance(this).unregisterReceiver(updateReceiver);
+        isReceiverRegistered = false;
+    }
 }

Likely an incorrect or invalid review comment.

app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java (1)

1-195: File unrelated to PR objectives

This database helper manages Connect messaging data, which is unrelated to the PersonalID lockout and backup code handling described in the PR objectives.

Likely an incorrect or invalid review comment.

app/src/org/commcare/models/connect/ConnectLoginJobListModel.java (1)

1-251: File unrelated to PR objectives

This model class for Connect job list items is unrelated to the PersonalID lockout functionality described in the PR objectives.

Likely an incorrect or invalid review comment.

app/src/org/commcare/fragments/connect/ConnectResultsSummaryListFragment.java (1)

1-296: File unrelated to PR objectives

This fragment displays Connect job payment summaries and confirmations, which doesn't align with the PR's purpose of handling PersonalID lockout after failed backup code attempts.

Likely an incorrect or invalid review comment.

app/src/org/commcare/activities/StandardHomeActivityUIController.java (4)

81-112: Well-structured job tile update logic

The implementation correctly handles job details display, including conditional visibility of working hours and proper null checks.


113-182: Comprehensive opportunity message handling

The method effectively handles all warning scenarios for Connect jobs, including multi-payment limits and job state transitions. The logic is thorough and well-structured.


183-218: Clean Connect progress UI integration

The refresh logic and progress update implementation are well-structured, properly managing the RecyclerView lifecycle and job progress display.


240-242: Correct migration to ConnectManager

The change from PersonalIdManager to ConnectManager.shouldShowJobStatus aligns with the broader refactoring across the codebase.

app/src/org/commcare/services/CommCareFirebaseMessagingService.java (3)

68-97: Well-refactored message handling

The updated onMessageReceived method properly handles both Connect (ccc_) and non-Connect actions, with appropriate notification display and data sync triggers.


106-119: Useful bitmap conversion utility

The getBitmapFromVectorDrawable method is a clean implementation for converting vector drawables to bitmaps, properly handling pre-Lollipop compatibility.


121-267: Comprehensive notification handling with Connect support

The refactored showNotification method effectively handles multiple notification types, including Connect messaging with encryption support and payment acknowledgments with action buttons. The selective notification suppression when users are actively viewing messaging UIs is a nice UX touch.

Verify that the notification channels are properly registered:

#!/bin/bash
# Description: Verify notification channel registration for messaging

# Check if the messaging notification channel is registered in CommCareNoficationManager
rg -A 5 "NOTIFICATION_CHANNEL_MESSAGING_ID|createNotificationChannel.*messaging"

# Verify the channel ID constant is defined
rg "NOTIFICATION_CHANNEL_MESSAGING_ID\s*="
app/src/org/commcare/adapters/JobListConnectHomeAppsAdapter.java (2)

29-77: Well-structured adapter with proper view type handling

The adapter correctly manages heterogeneous lists with separate ViewHolders for corrupt and non-corrupt jobs. The use of data binding and clear separation of concerns is commendable.


127-183: Comprehensive job UI state handling

The progress bar and job type configuration methods effectively handle all job states with appropriate visual feedback. The distinction between learning/delivery and various completion states is well-implemented.

app/src/org/commcare/connect/MessageManager.java (1)

35-69: Robust message handling with encryption support

The message and channel handling methods properly validate consent, manage encryption keys, and store messages securely. The error handling for missing keys is appropriate.

app/src/org/commcare/connect/network/ApiPersonalId.java (3)

117-138: Clean refactoring of linkHqWorker method

The simplified implementation with improved URL construction and conditional app record updates based on response codes is much cleaner than the previous version.


375-428: Secure encryption key management

The encryption key retrieval and handling methods properly manage keys both synchronously and asynchronously, with appropriate error handling and secure storage.


352-466: ```shell
#!/bin/bash

Verify that all ConnectMessage URL string resources are defined in the repository

rg -n -e 'ConnectMessageRetrieveMessagesURL'
-e 'ConnectMessageChannelConsentURL'
-e 'ConnectMessageConfirmURL'
-e 'ConnectMessageSendURL' .


</details>
<details>
<summary>app/src/org/commcare/connect/ConnectManager.java (1)</summary>

`1-628`: **Missing implementation for PR objectives**

The PR objectives describe handling user lockout after multiple failed backup code attempts and displaying appropriate messages when the server returns an `error_code` instead of `attempts_left`. However, this implementation is not present in this file. The PR also mentions a "PersonalID" feature flag that isn't referenced here.

Please verify if this is the correct file for PR #3216 "PersonalID Lockout" or if the lockout logic is implemented in other files.

```shell
#!/bin/bash
# Search for lockout-related code and PersonalID feature flag references
echo "Searching for lockout logic..."
rg -i "lockout|attempts_left|error_code" --type java -A 3

echo -e "\nSearching for PersonalID feature flag..."
rg -i "PersonalID.*feature.*flag|feature.*flag.*PersonalID" --type java -A 3

echo -e "\nSearching for backup code handling..."
rg -i "backup.*code" --type java -A 3
app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java (2)

62-64: Arguments access without null check is acceptable per user preference.

Based on retrieved learnings, 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.


182-187: Navigation options could cause issues with back stack.

The navigation options clear the entire back stack, which might not be appropriate in all scenarios and could leave users without a proper back navigation path.

Verify that clearing the entire navigation back stack is the intended behavior and doesn't break user navigation expectations.

#!/bin/bash
# Search for other uses of setPopUpTo with true, true to see if this pattern is used consistently
rg -A 3 -B 3 "setPopUpTo.*true.*true"

Comment on lines 227 to 237
try {
storage.removeAll(storage.getIDsForValues(new String[]{ConnectJobDeliveryFlagRecord.META_DELIVERY_ID},
new Object[]{deliveryId}));

storage.removeAll(storage.getIDsForValues(new String[]{ConnectJobDeliveryFlagRecord.META_DELIVERY_ID},
new Object[]{deliveryId}));

for (ConnectJobDeliveryFlagRecord incomingRecord : flags) {
storage.write(incomingRecord);
for (ConnectJobDeliveryFlagRecord incomingRecord : flags) {
storage.write(incomingRecord);
}
ConnectDatabaseHelper.connectDatabase.setTransactionSuccessful();
} finally {
ConnectDatabaseHelper.connectDatabase.endTransaction();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix transaction handling logic.

The transaction handling has been improved with try-finally, but there's a logical issue. setTransactionSuccessful() should only be called when the operations complete successfully, not in the finally block where it executes regardless of exceptions.

Apply this fix:

 ConnectDatabaseHelper.connectDatabase.beginTransaction();
 try {
     storage.removeAll(storage.getIDsForValues(new String[]{ConnectJobDeliveryFlagRecord.META_DELIVERY_ID},
             new Object[]{deliveryId}));

     for (ConnectJobDeliveryFlagRecord incomingRecord : flags) {
         storage.write(incomingRecord);
     }
+    ConnectDatabaseHelper.connectDatabase.setTransactionSuccessful();
 } finally {
-    ConnectDatabaseHelper.connectDatabase.setTransactionSuccessful();
     ConnectDatabaseHelper.connectDatabase.endTransaction();
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/org/commcare/connect/database/ConnectJobUtils.java around lines 227
to 237, the call to setTransactionSuccessful() is currently inside the try block
but should only be executed if no exceptions occur. To fix this, move
setTransactionSuccessful() so it is called only after all operations succeed and
before the finally block, ensuring it is not called if an exception is thrown.
Keep endTransaction() in the finally block to guarantee the transaction ends
properly.

Comment on lines 60 to 67
binding.declineButton.setOnClickListener(v -> {
channel.setAnsweredConsent(true);
channel.setConsented(false);
MessageManager.updateChannelConsent(requireContext(), channel, success -> {

});
NavHostFragment.findNavController(this).popBackStack();
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix inconsistent async handling in decline button.

The decline button immediately navigates without waiting for the updateChannelConsent callback, which could lead to race conditions or inconsistent state. Consider handling the callback properly or ensuring the operation completes before navigation.

         binding.declineButton.setOnClickListener(v -> {
             channel.setAnsweredConsent(true);
             channel.setConsented(false);
             MessageManager.updateChannelConsent(requireContext(), channel, success -> {
-
+                 // Always navigate back regardless of success for decline
+                 NavHostFragment.findNavController(this).popBackStack();
             });
-            NavHostFragment.findNavController(this).popBackStack();
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
binding.declineButton.setOnClickListener(v -> {
channel.setAnsweredConsent(true);
channel.setConsented(false);
MessageManager.updateChannelConsent(requireContext(), channel, success -> {
});
NavHostFragment.findNavController(this).popBackStack();
});
binding.declineButton.setOnClickListener(v -> {
channel.setAnsweredConsent(true);
channel.setConsented(false);
MessageManager.updateChannelConsent(requireContext(), channel, success -> {
// Always navigate back regardless of success for decline
NavHostFragment.findNavController(this).popBackStack();
});
});
🤖 Prompt for AI Agents
In
app/src/org/commcare/fragments/connectMessaging/ConnectMessageChannelConsentBottomSheet.java
around lines 60 to 67, the decline button's click listener triggers
updateChannelConsent asynchronously but immediately navigates away without
waiting for the update to complete. Modify the code to move the navigation call
inside the updateChannelConsent callback to ensure the consent update finishes
before popping the back stack, preventing race conditions and inconsistent
state.

Comment on lines 85 to 92
if(DateUtils.dateDiff(new Date(), lastDate) == 0) {
lastText = DateUtils.formatTime(lastDate, DateUtils.FORMAT_HUMAN_READABLE_SHORT);
} else {
SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMM, yyyy", Locale.ENGLISH);
lastText = outputFormat.format(lastDate);
}

binding.tvLastChatTime.setText(lastText);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add null safety checks for date operations.

The date formatting logic should include null checks and exception handling to prevent crashes if date parsing fails or if lastDate becomes null unexpectedly.

             if(showDate) {
                 String lastText;
-                if(DateUtils.dateDiff(new Date(), lastDate) == 0) {
-                    lastText = DateUtils.formatTime(lastDate, DateUtils.FORMAT_HUMAN_READABLE_SHORT);
-                } else {
-                    SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMM, yyyy", Locale.ENGLISH);
-                    lastText = outputFormat.format(lastDate);
-                }
+                try {
+                    if(DateUtils.dateDiff(new Date(), lastDate) == 0) {
+                        lastText = DateUtils.formatTime(lastDate, DateUtils.FORMAT_HUMAN_READABLE_SHORT);
+                    } else {
+                        SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMM, yyyy", Locale.ENGLISH);
+                        lastText = outputFormat.format(lastDate);
+                    }
+                } catch (Exception e) {
+                    lastText = ""; // Fallback for date formatting errors
+                }
 
                 binding.tvLastChatTime.setText(lastText);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(DateUtils.dateDiff(new Date(), lastDate) == 0) {
lastText = DateUtils.formatTime(lastDate, DateUtils.FORMAT_HUMAN_READABLE_SHORT);
} else {
SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMM, yyyy", Locale.ENGLISH);
lastText = outputFormat.format(lastDate);
}
binding.tvLastChatTime.setText(lastText);
if (showDate) {
String lastText;
try {
if (DateUtils.dateDiff(new Date(), lastDate) == 0) {
lastText = DateUtils.formatTime(lastDate, DateUtils.FORMAT_HUMAN_READABLE_SHORT);
} else {
SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMM, yyyy", Locale.ENGLISH);
lastText = outputFormat.format(lastDate);
}
} catch (Exception e) {
lastText = ""; // Fallback for date formatting errors
}
binding.tvLastChatTime.setText(lastText);
}
🤖 Prompt for AI Agents
In app/src/org/commcare/adapters/ChannelAdapter.java around lines 85 to 92, add
null checks before performing date operations to ensure lastDate is not null.
Wrap the date formatting logic in a try-catch block to handle potential
exceptions during date parsing or formatting. If lastDate is null or an
exception occurs, set lastText to a safe default string to prevent crashes.

public void setType(int type) {
this.type = type;
}
public Date getTimestamp() { return timestamp; }
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add missing setter method for timestamp field.

The timestamp field has a getter but no corresponding setter method, which breaks the consistency of the data class pattern.

 public Date getTimestamp() { return timestamp; }
+
+public void setTimestamp(Date timestamp) {
+    this.timestamp = timestamp;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public Date getTimestamp() { return timestamp; }
public Date getTimestamp() { return timestamp; }
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connectMessaging/ConnectMessageChatData.java
at line 31, add a setter method for the timestamp field to complement the
existing getter. Define a public method named setTimestamp that takes a Date
parameter and assigns it to the timestamp field, ensuring the class follows the
standard data class pattern with both getter and setter methods.

Comment on lines 277 to 286
public void updateConnectJobProgress() {
ConnectJobRecord job = ConnectManager.getActiveJob();
if(job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) {
ConnectManager.updateDeliveryProgress(this, job, success -> {
if (success) {
uiController.updateConnectProgress();
}
});
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for Connect operations.

The updateConnectJobProgress() method integrates well with the existing sync flow, but lacks error handling for potential exceptions from ConnectManager operations.

 public void updateConnectJobProgress() {
-    ConnectJobRecord job = ConnectManager.getActiveJob();
-    if(job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) {
-        ConnectManager.updateDeliveryProgress(this, job, success -> {
-            if (success) {
-                uiController.updateConnectProgress();
-            }
-        });
+    try {
+        ConnectJobRecord job = ConnectManager.getActiveJob();
+        if(job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) {
+            ConnectManager.updateDeliveryProgress(this, job, success -> {
+                if (success) {
+                    uiController.updateConnectProgress();
+                }
+            });
+        }
+    } catch (Exception e) {
+        Log.w(TAG, "Failed to update Connect job progress", e);
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void updateConnectJobProgress() {
ConnectJobRecord job = ConnectManager.getActiveJob();
if(job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) {
ConnectManager.updateDeliveryProgress(this, job, success -> {
if (success) {
uiController.updateConnectProgress();
}
});
}
}
public void updateConnectJobProgress() {
try {
ConnectJobRecord job = ConnectManager.getActiveJob();
if (job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) {
ConnectManager.updateDeliveryProgress(this, job, success -> {
if (success) {
uiController.updateConnectProgress();
}
});
}
} catch (Exception e) {
Log.w(TAG, "Failed to update Connect job progress", e);
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/activities/StandardHomeActivity.java around lines 277 to
286, the updateConnectJobProgress method calls ConnectManager methods without
handling potential exceptions. Add try-catch blocks around the ConnectManager
calls to catch and handle any exceptions, ensuring the app remains stable and
errors are logged or managed appropriately.

Comment on lines 101 to 106
} catch (JSONException e) {
throw new RuntimeException(e);
} catch (IOException e) {
Toast.makeText(requireContext(), R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
Logger.exception("Parsing return from Opportunities request", e);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve exception handling for JSON parsing.

Rethrowing JSONException as RuntimeException is inappropriate and inconsistent with other error handling in the same method.

                 } catch (JSONException e) {
-                    throw new RuntimeException(e);
+                    Toast.makeText(requireContext(), R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
+                    Logger.exception("Parsing return from Opportunities request", e);
                 } catch (IOException e) {
                     Toast.makeText(requireContext(), R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
                     Logger.exception("Parsing return from Opportunities request", e);
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (JSONException e) {
throw new RuntimeException(e);
} catch (IOException e) {
Toast.makeText(requireContext(), R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
Logger.exception("Parsing return from Opportunities request", e);
}
} catch (JSONException e) {
Toast.makeText(requireContext(), R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
Logger.exception("Parsing return from Opportunities request", e);
} catch (IOException e) {
Toast.makeText(requireContext(), R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
Logger.exception("Parsing return from Opportunities request", e);
}
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java around
lines 101 to 106, the JSONException is currently rethrown as a RuntimeException,
which is inconsistent with the IOException handling that shows a user-friendly
toast and logs the error. Modify the JSONException catch block to handle the
exception similarly by showing an appropriate toast message to the user and
logging the exception instead of rethrowing it.

Comment on lines 156 to 162
if(!Strings.isNullOrEmpty(opportunityId)) {
int jobId = Integer.parseInt(opportunityId);
ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
if(job != null) {
ConnectManager.setActiveJob(job);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add safe parsing for opportunity ID.

The Integer.parseInt() call could throw NumberFormatException if the opportunityId contains non-numeric characters.

         if(!Strings.isNullOrEmpty(opportunityId)) {
-            int jobId = Integer.parseInt(opportunityId);
-            ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
-            if(job != null) {
-                ConnectManager.setActiveJob(job);
+            try {
+                int jobId = Integer.parseInt(opportunityId);
+                ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
+                if(job != null) {
+                    ConnectManager.setActiveJob(job);
+                }
+            } catch (NumberFormatException e) {
+                Logger.log("ConnectUnlock", "Invalid opportunity ID format: " + opportunityId);
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!Strings.isNullOrEmpty(opportunityId)) {
int jobId = Integer.parseInt(opportunityId);
ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
if(job != null) {
ConnectManager.setActiveJob(job);
}
}
if (!Strings.isNullOrEmpty(opportunityId)) {
try {
int jobId = Integer.parseInt(opportunityId);
ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
if (job != null) {
ConnectManager.setActiveJob(job);
}
} catch (NumberFormatException e) {
Logger.log("ConnectUnlock", "Invalid opportunity ID format: " + opportunityId);
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java around
lines 156 to 162, the Integer.parseInt(opportunityId) call can throw a
NumberFormatException if opportunityId contains non-numeric characters. To fix
this, wrap the parseInt call in a try-catch block to safely handle
NumberFormatException. If an exception occurs, handle it gracefully by either
logging the error or skipping the job retrieval logic to prevent the app from
crashing.

⚠️ Potential issue

Potential NumberFormatException not handled.

Integer.parseInt(opportunityId) could throw NumberFormatException if opportunityId contains non-numeric characters.

         if(!Strings.isNullOrEmpty(opportunityId)) {
-            int jobId = Integer.parseInt(opportunityId);
-            ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
-            if(job != null) {
-                ConnectManager.setActiveJob(job);
+            try {
+                int jobId = Integer.parseInt(opportunityId);
+                ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
+                if(job != null) {
+                    ConnectManager.setActiveJob(job);
+                }
+            } catch (NumberFormatException e) {
+                Logger.log("ConnectUnlockFragment", "Invalid opportunity ID format: " + opportunityId);
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if(!Strings.isNullOrEmpty(opportunityId)) {
int jobId = Integer.parseInt(opportunityId);
ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
if(job != null) {
ConnectManager.setActiveJob(job);
}
}
if (!Strings.isNullOrEmpty(opportunityId)) {
try {
int jobId = Integer.parseInt(opportunityId);
ConnectJobRecord job = ConnectJobUtils.getCompositeJob(requireContext(), jobId);
if (job != null) {
ConnectManager.setActiveJob(job);
}
} catch (NumberFormatException e) {
Logger.log("ConnectUnlockFragment", "Invalid opportunity ID format: " + opportunityId);
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java around
lines 156 to 162, the call to Integer.parseInt(opportunityId) can throw a
NumberFormatException if opportunityId contains non-numeric characters. To fix
this, wrap the parseInt call in a try-catch block that catches
NumberFormatException and handles it gracefully, such as logging the error or
skipping the job setup, to prevent the app from crashing.


@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ConnectJobRecord job = ConnectManager.getActiveJob();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing null check for active job

ConnectManager.getActiveJob() could return null, but it's used without checking in multiple places.

Add null safety checks:

 @Override
 public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
     ConnectJobRecord job = ConnectManager.getActiveJob();
+    if (job == null) {
+        return;
+    }
 @Override
 public int getItemCount() {
     ConnectJobRecord job = ConnectManager.getActiveJob();
-    return showPayments ? job.getPayments().size() : job.getDeliveries().size();
+    return job != null ? (showPayments ? job.getPayments().size() : job.getDeliveries().size()) : 0;
 }

Also applies to: 116-117

🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java at
line 86 and also lines 116-117, the call to ConnectManager.getActiveJob() may
return null but is used without null checks. Add a null check after retrieving
the active job to ensure it is not null before proceeding. If the job is null,
handle that case appropriately to avoid NullPointerExceptions.

Comment on lines 149 to 168
public boolean updateConfirmedText(Context context, ConnectJobPaymentRecord payment) {
boolean enabled;
int confirmTextId;
if(payment.getConfirmed()) {
enabled = payment.allowConfirmUndo();
confirmTextId = enabled ?
R.string.connect_results_payment_confirm_undo :
R.string.connect_results_payment_confirmed;
} else {
enabled = payment.allowConfirm();
confirmTextId = enabled ?
R.string.connect_results_payment_confirm :
R.string.connect_results_payment_not_confirmed;
}

confirmText.setText(confirmTextId);
confirmText.setTextColor(context.getResources().getColor(enabled ? R.color.blue : R.color.dark_grey));

return enabled;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Business logic in ViewHolder

ViewHolders should only handle view binding, not business logic like permission checks.

Move the logic to the adapter or a separate handler class:

-public boolean updateConfirmedText(Context context, ConnectJobPaymentRecord payment) {
-    boolean enabled;
-    int confirmTextId;
-    if(payment.getConfirmed()) {
-        enabled = payment.allowConfirmUndo();
-        confirmTextId = enabled ?
-                R.string.connect_results_payment_confirm_undo :
-                R.string.connect_results_payment_confirmed;
-    } else {
-        enabled = payment.allowConfirm();
-        confirmTextId = enabled ?
-                R.string.connect_results_payment_confirm :
-                R.string.connect_results_payment_not_confirmed;
-    }
-
-    confirmText.setText(confirmTextId);
-    confirmText.setTextColor(context.getResources().getColor(enabled ? R.color.blue : R.color.dark_grey));
-
-    return enabled;
-}
+public void bindConfirmText(int textResId, boolean enabled, Context context) {
+    confirmText.setText(textResId);
+    confirmText.setTextColor(context.getResources().getColor(enabled ? R.color.blue : R.color.dark_grey));
+}
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java around
lines 149 to 168, the updateConfirmedText method in the ViewHolder contains
business logic related to permission checks, which should be moved out of the
ViewHolder. Refactor by moving the logic that determines the enabled state and
confirmTextId to the adapter or a separate handler class, and have the
ViewHolder only handle setting the text and color on the views based on
parameters passed from the adapter or handler.

} else if(holder instanceof PaymentViewHolder paymentHolder) {
final ConnectJobPaymentRecord payment = job.getPayments().get(position);

String money = job.getMoneyString(Integer.parseInt(payment.getAmount()));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Unsafe integer parsing

Parsing payment amount without catching NumberFormatException.

Add proper error handling:

-String money = job.getMoneyString(Integer.parseInt(payment.getAmount()));
+String money;
+try {
+    money = job.getMoneyString(Integer.parseInt(payment.getAmount()));
+} catch (NumberFormatException e) {
+    money = job.getMoneyString(0); // or handle appropriately
+}
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java at
line 97, the code parses an integer from payment.getAmount() without handling
potential NumberFormatException. Wrap the Integer.parseInt call in a try-catch
block to catch NumberFormatException and handle it gracefully, such as by
logging the error and providing a default value or skipping the operation to
prevent the app from crashing.

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.

Review continued from previous batch...

@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
parentContext = parent.getContext();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential memory leak from storing context

Storing the parent context as a field in the adapter can lead to memory leaks if the adapter outlives the activity/fragment.

Consider passing the context as a parameter to methods that need it instead:

-private Context parentContext;

 @Override
 public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-    parentContext = parent.getContext();
+    Context context = parent.getContext();
     if(showPayments) {
-        return new PaymentViewHolder(LayoutInflater.from(parent.getContext())
+        return new PaymentViewHolder(LayoutInflater.from(context)
                 .inflate(R.layout.connect_payment_item, parent, false));
     } else {
-        return new VerificationViewHolder(LayoutInflater.from(parent.getContext())
+        return new VerificationViewHolder(LayoutInflater.from(context)
                 .inflate(R.layout.connect_verification_item, parent, false));
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
parentContext = parent.getContext();
// In ConnectResultsListFragment.java, remove the field declaration:
// private Context parentContext;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Context context = parent.getContext();
if (showPayments) {
return new PaymentViewHolder(
LayoutInflater.from(context)
.inflate(R.layout.connect_payment_item, parent, false)
);
} else {
return new VerificationViewHolder(
LayoutInflater.from(context)
.inflate(R.layout.connect_verification_item, parent, false)
);
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectResultsListFragment.java at
line 74, avoid storing the parent context as a field to prevent memory leaks.
Instead of assigning parent.getContext() to a field, refactor the code to pass
the context directly as a parameter to methods that require it, eliminating the
need to keep a long-lived reference to the context.

Comment on lines 217 to 222
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayShowHomeEnabled(isBackEnabled());
actionBar.setDisplayHomeAsUpEnabled(isBackEnabled());
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing null check for ActionBar

The method checks for null but should handle the case where actionBar is null.

 private void updateBackButton() {
     ActionBar actionBar = getSupportActionBar();
     if (actionBar != null) {
         actionBar.setDisplayShowHomeEnabled(isBackEnabled());
         actionBar.setDisplayHomeAsUpEnabled(isBackEnabled());
+    } else {
+        // Log warning or handle the case where action bar is not available
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayShowHomeEnabled(isBackEnabled());
actionBar.setDisplayHomeAsUpEnabled(isBackEnabled());
}
}
private void updateBackButton() {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayShowHomeEnabled(isBackEnabled());
actionBar.setDisplayHomeAsUpEnabled(isBackEnabled());
} else {
// Log warning or handle the case where action bar is not available
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/activities/connect/ConnectActivity.java around lines 217
to 222, the code checks if actionBar is null but does not handle the null case
explicitly. Add an else block or appropriate handling logic for when actionBar
is null to ensure the app behaves correctly and avoids potential issues.

Comment on lines 88 to 131
public static void storeMessagingChannels(Context context, List<ConnectMessagingChannelRecord> channels, boolean pruneMissing) {
SqlStorage<ConnectMessagingChannelRecord> storage = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class);

List<ConnectMessagingChannelRecord> existingList = getMessagingChannels(context);

//Delete payments that are no longer available
Vector<Integer> recordIdsToDelete = new Vector<>();
for (ConnectMessagingChannelRecord existing : existingList) {
boolean stillExists = false;
for (ConnectMessagingChannelRecord incoming : channels) {
if (existing.getChannelId().equals(incoming.getChannelId())) {
incoming.setID(existing.getID());

incoming.setChannelCreated(existing.getChannelCreated());

if(!incoming.getAnsweredConsent()) {
incoming.setAnsweredConsent(existing.getAnsweredConsent());
}

if(existing.getKey().length() > 0) {
incoming.setKey(existing.getKey());
}

stillExists = true;
break;
}
}

if (!stillExists && pruneMissing) {
//Mark the delivery for deletion
//Remember the ID so we can delete them all at once after the loop
recordIdsToDelete.add(existing.getID());
}
}

if (pruneMissing) {
storage.removeAll(recordIdsToDelete);
}

//Now insert/update deliveries
for (ConnectMessagingChannelRecord incomingRecord : channels) {
storage.write(incomingRecord);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing database transaction

Multiple database operations without transaction wrapping could lead to inconsistent state.

Wrap operations in a transaction:

 public static void storeMessagingChannels(Context context, List<ConnectMessagingChannelRecord> channels, boolean pruneMissing) {
     SqlStorage<ConnectMessagingChannelRecord> storage = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class);
+    
+    storage.beginTransaction();
+    try {
         // ... existing code ...
+        storage.setTransactionSuccessful();
+    } finally {
+        storage.endTransaction();
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static void storeMessagingChannels(Context context, List<ConnectMessagingChannelRecord> channels, boolean pruneMissing) {
SqlStorage<ConnectMessagingChannelRecord> storage = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class);
List<ConnectMessagingChannelRecord> existingList = getMessagingChannels(context);
//Delete payments that are no longer available
Vector<Integer> recordIdsToDelete = new Vector<>();
for (ConnectMessagingChannelRecord existing : existingList) {
boolean stillExists = false;
for (ConnectMessagingChannelRecord incoming : channels) {
if (existing.getChannelId().equals(incoming.getChannelId())) {
incoming.setID(existing.getID());
incoming.setChannelCreated(existing.getChannelCreated());
if(!incoming.getAnsweredConsent()) {
incoming.setAnsweredConsent(existing.getAnsweredConsent());
}
if(existing.getKey().length() > 0) {
incoming.setKey(existing.getKey());
}
stillExists = true;
break;
}
}
if (!stillExists && pruneMissing) {
//Mark the delivery for deletion
//Remember the ID so we can delete them all at once after the loop
recordIdsToDelete.add(existing.getID());
}
}
if (pruneMissing) {
storage.removeAll(recordIdsToDelete);
}
//Now insert/update deliveries
for (ConnectMessagingChannelRecord incomingRecord : channels) {
storage.write(incomingRecord);
}
}
public static void storeMessagingChannels(Context context, List<ConnectMessagingChannelRecord> channels, boolean pruneMissing) {
SqlStorage<ConnectMessagingChannelRecord> storage = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class);
storage.beginTransaction();
try {
List<ConnectMessagingChannelRecord> existingList = getMessagingChannels(context);
//Delete channels that are no longer available
Vector<Integer> recordIdsToDelete = new Vector<>();
for (ConnectMessagingChannelRecord existing : existingList) {
boolean stillExists = false;
for (ConnectMessagingChannelRecord incoming : channels) {
if (existing.getChannelId().equals(incoming.getChannelId())) {
incoming.setID(existing.getID());
incoming.setChannelCreated(existing.getChannelCreated());
if (!incoming.getAnsweredConsent()) {
incoming.setAnsweredConsent(existing.getAnsweredConsent());
}
if (existing.getKey().length() > 0) {
incoming.setKey(existing.getKey());
}
stillExists = true;
break;
}
}
if (!stillExists && pruneMissing) {
//Mark the channel for deletion
recordIdsToDelete.add(existing.getID());
}
}
if (pruneMissing) {
storage.removeAll(recordIdsToDelete);
}
//Now insert/update channels
for (ConnectMessagingChannelRecord incomingRecord : channels) {
storage.write(incomingRecord);
}
storage.setTransactionSuccessful();
} finally {
storage.endTransaction();
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java
around lines 88 to 131, the method performs multiple database operations without
wrapping them in a transaction, risking inconsistent state. To fix this, wrap
the deletion and insertion/update operations inside a single database
transaction using storage's transaction methods, ensuring atomicity and
consistency of the changes.

Comment on lines 23 to 35
public static List<ConnectMessagingChannelRecord> getMessagingChannels(Context context) {
List<ConnectMessagingChannelRecord> channels = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class)
.getRecordsForValues(new String[]{}, new Object[]{});

Map<String, ConnectMessagingChannelRecord> channelMap = new HashMap<>();
for (ConnectMessagingChannelRecord c : channels) {
channelMap.put(c.getChannelId(), c);
}
for (ConnectMessagingMessageRecord connectMessagingMessageRecord : getMessagingMessagesAll(context)) {
ConnectMessagingChannelRecord connectMessagingChannelRecord = channelMap.get(connectMessagingMessageRecord.getChannelId());
connectMessagingChannelRecord.getMessages().add(connectMessagingMessageRecord);
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Performance issue: N+1 query pattern

Loading all messages for all channels in memory could cause performance issues with large datasets.

Consider loading messages lazily or with pagination:

-for (ConnectMessagingMessageRecord connectMessagingMessageRecord : getMessagingMessagesAll(context)) {
-    ConnectMessagingChannelRecord connectMessagingChannelRecord = channelMap.get(connectMessagingMessageRecord.getChannelId());
-    connectMessagingChannelRecord.getMessages().add(connectMessagingMessageRecord);
-}
+// Load only the latest message per channel for preview
+// Load full messages when channel is opened
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static List<ConnectMessagingChannelRecord> getMessagingChannels(Context context) {
List<ConnectMessagingChannelRecord> channels = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class)
.getRecordsForValues(new String[]{}, new Object[]{});
Map<String, ConnectMessagingChannelRecord> channelMap = new HashMap<>();
for (ConnectMessagingChannelRecord c : channels) {
channelMap.put(c.getChannelId(), c);
}
for (ConnectMessagingMessageRecord connectMessagingMessageRecord : getMessagingMessagesAll(context)) {
ConnectMessagingChannelRecord connectMessagingChannelRecord = channelMap.get(connectMessagingMessageRecord.getChannelId());
connectMessagingChannelRecord.getMessages().add(connectMessagingMessageRecord);
}
public static List<ConnectMessagingChannelRecord> getMessagingChannels(Context context) {
List<ConnectMessagingChannelRecord> channels = ConnectDatabaseHelper.getConnectStorage(context, ConnectMessagingChannelRecord.class)
.getRecordsForValues(new String[]{}, new Object[]{});
Map<String, ConnectMessagingChannelRecord> channelMap = new HashMap<>();
for (ConnectMessagingChannelRecord c : channels) {
channelMap.put(c.getChannelId(), c);
}
// Load only the latest message per channel for preview
// Load full messages when channel is opened
}
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/database/ConnectMessagingDatabaseHelper.java
around lines 23 to 35, the current code loads all messages for all channels
eagerly, causing an N+1 query performance issue with large datasets. Refactor
the method to avoid loading all messages at once by implementing lazy loading or
pagination for messages. This can be done by modifying the data retrieval logic
to fetch messages on demand or in smaller batches rather than loading all
messages into memory simultaneously.

/**
* Model class for storing list item data
*/
public class ConnectLoginJobListModel implements Parcelable {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incomplete Parcelable implementation

The class implements Parcelable but only serializes 4 fields (name, id, description, organization) out of 16+ fields. This will cause data loss when passing between components. According to previous feedback, this class doesn't need Parcelable as it's not passed between activities/fragments.

Either implement Parcelable completely or remove it:

-public class ConnectLoginJobListModel implements Parcelable {
+public class ConnectLoginJobListModel {

And remove all Parcelable-related methods (lines 79-109).

Also applies to: 79-93

🤖 Prompt for AI Agents
In app/src/org/commcare/models/connect/ConnectLoginJobListModel.java at lines 13
and 79-109, the class implements Parcelable but only serializes a subset of its
fields, causing data loss. Since this class is not passed between activities or
fragments, remove the Parcelable implementation entirely, including the
interface declaration and all related methods in lines 79-109, to avoid
incomplete serialization and potential bugs.

Comment on lines 129 to 131
if (delivery.getUnitName().equalsIgnoreCase(unitName) &&
(currentFilter.equals(ALL_IDENTIFIER) ||
delivery.getStatus().equalsIgnoreCase(currentFilter))) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Case-insensitive string comparison could cause issues.

Using equalsIgnoreCase() for status comparison might not be appropriate if the status values should have exact case matching. This could lead to unexpected behavior if the API returns status values with different casing.

Consider whether case-insensitive comparison is appropriate here:

                 if (delivery.getUnitName().equalsIgnoreCase(unitName) &&
                         (currentFilter.equals(ALL_IDENTIFIER) ||
-                                delivery.getStatus().equalsIgnoreCase(currentFilter))) {
+                                delivery.getStatus().equals(currentFilter))) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (delivery.getUnitName().equalsIgnoreCase(unitName) &&
(currentFilter.equals(ALL_IDENTIFIER) ||
delivery.getStatus().equalsIgnoreCase(currentFilter))) {
if (delivery.getUnitName().equalsIgnoreCase(unitName) &&
(currentFilter.equals(ALL_IDENTIFIER) ||
delivery.getStatus().equals(currentFilter))) {
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectDeliveryListFragment.java
around lines 129 to 131, the use of equalsIgnoreCase() for comparing delivery
status may cause issues if status values require exact case matching. Review the
expected case sensitivity of the status values from the API and replace
equalsIgnoreCase() with equals() if exact case matching is needed to prevent
unexpected behavior.

Comment on lines 74 to 78
private void startAppDownload() {
ConnectJobRecord job = ConnectManager.getActiveJob();
ConnectAppRecord record = getLearnApp ? job.getLearnAppInfo() : job.getDeliveryAppInfo();
ConnectManager.downloadAppOrResumeUpdates(record.getInstallUrl(), this);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential null pointer exceptions in startAppDownload.

Multiple potential null pointer exceptions: ConnectManager.getActiveJob() could return null, and getLearnApp ? job.getLearnAppInfo() : job.getDeliveryAppInfo() could also return null.

     private void startAppDownload() {
         ConnectJobRecord job = ConnectManager.getActiveJob();
+        if (job == null) {
+            showInstallFailError(AppInstallStatus.UnknownFailure);
+            return;
+        }
         ConnectAppRecord record = getLearnApp ? job.getLearnAppInfo() : job.getDeliveryAppInfo();
+        if (record == null) {
+            showInstallFailError(AppInstallStatus.UnknownFailure);
+            return;
+        }
         ConnectManager.downloadAppOrResumeUpdates(record.getInstallUrl(), this);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void startAppDownload() {
ConnectJobRecord job = ConnectManager.getActiveJob();
ConnectAppRecord record = getLearnApp ? job.getLearnAppInfo() : job.getDeliveryAppInfo();
ConnectManager.downloadAppOrResumeUpdates(record.getInstallUrl(), this);
}
private void startAppDownload() {
ConnectJobRecord job = ConnectManager.getActiveJob();
if (job == null) {
showInstallFailError(AppInstallStatus.UnknownFailure);
return;
}
ConnectAppRecord record = getLearnApp ? job.getLearnAppInfo() : job.getDeliveryAppInfo();
if (record == null) {
showInstallFailError(AppInstallStatus.UnknownFailure);
return;
}
ConnectManager.downloadAppOrResumeUpdates(record.getInstallUrl(), this);
}
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java around
lines 74 to 78, the method startAppDownload does not check for null values which
can cause null pointer exceptions. Add null checks after calling
ConnectManager.getActiveJob() to ensure job is not null before accessing its
methods. Also, verify that the result of getLearnApp ? job.getLearnAppInfo() :
job.getDeliveryAppInfo() is not null before passing it to
ConnectManager.downloadAppOrResumeUpdates. Handle these null cases gracefully,
such as by logging an error or returning early.

Comment on lines 107 to 109
setWaitDialogEnabled(true);
((ConnectActivity)getActivity()).startAppValidation();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Unsafe casting without null check.

The cast to ConnectActivity is done without checking if the activity is null or of the correct type.

     private void onSuccessfulInstall() {
         setWaitDialogEnabled(true);
-        ((ConnectActivity)getActivity()).startAppValidation();
+        Activity activity = getActivity();
+        if (activity instanceof ConnectActivity connectActivity) {
+            connectActivity.startAppValidation();
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setWaitDialogEnabled(true);
((ConnectActivity)getActivity()).startAppValidation();
}
private void onSuccessfulInstall() {
setWaitDialogEnabled(true);
Activity activity = getActivity();
if (activity instanceof ConnectActivity connectActivity) {
connectActivity.startAppValidation();
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java around
lines 107 to 109, the code casts getActivity() to ConnectActivity without
verifying if getActivity() is null or an instance of ConnectActivity. To fix
this, add a null check for getActivity() and use an instanceof check before
casting. Only call startAppValidation() if the activity is non-null and of the
correct type to avoid potential ClassCastException or NullPointerException.

Comment on lines 183 to 189
public void failWithNotification(AppInstallStatus statusfailstate) {
if (statusfailstate == AppInstallStatus.DuplicateApp) {
onSuccessfulInstall();
} else {
showInstallFailError(statusfailstate);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inconsistent error handling in failWithNotification.

The method handles DuplicateApp as a success case, but this might not be appropriate in all contexts. Consider if this logic should be more explicit about when duplicate apps are acceptable.

Consider making the duplicate app handling more explicit:

     @Override
     public void failWithNotification(AppInstallStatus statusfailstate) {
         if (statusfailstate == AppInstallStatus.DuplicateApp) {
+            // Log that we're treating duplicate app as success
+            Logger.log("ConnectDownloading", "Duplicate app detected, treating as successful install");
             onSuccessfulInstall();
         } else {
             showInstallFailError(statusfailstate);
         }
     }
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectDownloadingFragment.java around
lines 183 to 189, the failWithNotification method treats DuplicateApp status as
a success without clear context. Refactor this method to explicitly check the
context or conditions under which DuplicateApp should be considered successful,
possibly by adding a parameter or a separate method to handle this case. Ensure
the logic clearly distinguishes when DuplicateApp is acceptable and when it
should trigger an error, improving clarity and maintainability.

Comment on lines 101 to 103
} catch (JSONException e) {
throw new RuntimeException(e);
} catch (IOException e) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

RuntimeException wrapping JSONException loses context.

Wrapping JSONException in RuntimeException loses the original exception context and stack trace, making debugging more difficult.

                 } catch (JSONException e) {
-                    throw new RuntimeException(e);
+                    Logger.exception("Failed to parse JSON response from opportunities API", e);
+                    Toast.makeText(requireContext(), R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
                 } catch (IOException e) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (JSONException e) {
throw new RuntimeException(e);
} catch (IOException e) {
} catch (JSONException e) {
Logger.exception("Failed to parse JSON response from opportunities API", e);
Toast.makeText(requireContext(), R.string.connect_job_list_api_failure, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
🤖 Prompt for AI Agents
In app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java around
lines 101 to 103, avoid wrapping JSONException in a generic RuntimeException as
it loses the original exception context and stack trace. Instead, rethrow the
JSONException directly or wrap it in a custom exception that preserves the
original exception as the cause to maintain full debugging information.

@shubham1g5 shubham1g5 changed the base branch from master to june_beta_hotfix June 25, 2025 02:12
Comment on lines -205 to +207
} else if (sessionData.getAttemptsLeft() != null && sessionData.getAttemptsLeft() == 0) {
navigateWithMessage(getString(R.string.personalid_recovery_failed_title),
getString(R.string.personalid_recovery_failed_message),
ConnectConstants.PERSONALID_RECOVERY_ACCOUNT_ORPHANED);
} else if (sessionData.getSessionFailureCode() != null &&
sessionData.getSessionFailureCode().equalsIgnoreCase("LOCKED_ACCOUNT")) {
handleAccountLockout();
Copy link
Contributor

Choose a reason for hiding this comment

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

If server keep sending both attempts_left and error_code and if app checks first for error_code before attempts_left, it should support both versions of users?

Copy link
Contributor

Choose a reason for hiding this comment

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

we don't really want to support orphaned account any more which is why we are removing attempts_left ==0 check.

@shubham1g5 shubham1g5 marked this pull request as ready for review June 25, 2025 11:12
@shubham1g5 shubham1g5 merged commit 6415aec into june_beta_hotfix Jun 25, 2025
2 checks passed
@shubham1g5 shubham1g5 deleted the dv/personalid_lockout branch June 25, 2025 11:12
This was referenced Jul 14, 2025
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.

4 participants