Skip to content

Conversation

@avazirna
Copy link
Contributor

@avazirna avazirna commented Oct 20, 2025

Product Description

Backmerge Commcare 2.60.0

cross-request: dimagi/commcare-core#1507

shubham1g5 and others added 30 commits October 7, 2025 12:06
Hide Notifications behind a feature flag
…s we are using both data types in different places in code
Fixes crash on clicking View Job Status
Correct error parsing for sub code
…-issue

Fix FCMMssageData serialization issue
…ig page.

Fixed issue where fingerprint option could be shown when device doesn't have a fingerprint scanner.
@avazirna
Copy link
Contributor Author

@damagatchi retest this please

@coderabbitai
Copy link

coderabbitai bot commented Oct 20, 2025

📝 Walkthrough

Walkthrough

This pull request introduces several interconnected changes across the Connect feature, network error handling, and Personal ID biometric verification flows. The manifest version is downgraded from 2.61 to 2.60.0. The messaging menu item is removed from the Connect menu. Localization strings are updated across multiple locales to standardize "PersonalID" capitalization and add separate unlock guidance messages for PIN and fingerprint methods. Network error handling is systematically refactored to expose error streams as strings through a new NetworkUtils utility, affecting the IApiCallback interface and implementations. The NOTIFICATIONS feature flag is introduced and integrated into drawer visibility logic. Personal ID biometric configuration UI is refactored to conditionally display PIN or fingerprint options with corresponding messages. Job status preservation logic prevents downgrading when updating existing Connect jobs. Various exception handlers now log errors instead of silently suppressing them.

Sequence Diagram(s)

sequenceDiagram
    participant Client as HTTP Client
    participant API as Network Layer
    participant Callback as IApiCallback
    participant Handler as Error Handler
    
    Client->>API: Make Request
    API->>API: Receive Error Response
    API->>API: Extract Error Body (InputStream → String)
    API->>API: Parse Error Codes from String
    API->>Callback: processFailure(responseCode, url, errorBody)
    Callback->>Handler: Handle specific error code
    Handler-->>Client: Error Resolution/Retry
Loading
sequenceDiagram
    participant Fragment as PersonalIdBiometricConfigFragment
    participant UI as UI Elements
    
    Fragment->>Fragment: updateUiBasedOnMinSecurityRequired()
    Fragment->>Fragment: Determine button visibility<br/>(fingerprint/PIN config status)
    Fragment->>Fragment: Select message based on<br/>hardware availability
    Fragment->>Fragment: Compute title based on<br/>active buttons
    Fragment->>UI: Bind title, message, button states
    UI-->>Fragment: UI Updated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

The PR contains heterogeneous changes distributed across network callbacks, UI fragments, localization resources, and database models. While the localization updates are repetitive (reducing cognitive load), the systematic refactoring of error handling signatures across multiple API callback implementations and activities requires careful verification that all call sites are updated consistently. The feature flag introduction and UI refactoring in PersonalIdBiometricConfigFragment demand understanding of the conditional logic flow. The removal of messaging UI from ConnectActivity requires tracing to ensure no orphaned references remain.

Possibly related PRs

Suggested labels

skip-integration-tests

Suggested reviewers

  • Jignesh-dimagi
  • shubham1g5

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description is severely incomplete compared to the provided template. It contains only a minimal "Product Description" section stating "Backmerge Commcare 2.60.0" and is missing all other required sections: Technical Summary (no link to ticket or rationale), Feature Flag section, the entire Safety Assurance section (safety story, automated test coverage, and QA plan), and the Labels and Review checklist. For a complex backmerge involving version changes, API signature modifications, UI refactoring, and extensive localization updates, the description provides virtually no context about the rationale, safety considerations, or testing performed. The author should expand the PR description to follow the repository template by adding: (1) a more detailed Product Description explaining the visible changes to users, (2) a Technical Summary with links to relevant tickets or documentation and the rationale behind the backmerge, (3) identification of any feature flags affected, (4) a comprehensive Safety Assurance section documenting local testing and blast radius analysis, (5) details on automated test coverage and the QA plan, and (6) completion of the Labels and Review checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.75% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "Backmerge Commcare 2.60.0" accurately describes the nature of the pull request—it represents a backmerge operation from the commcare_2.60 branch to the master branch. The title directly refers to the actual operation being performed and is appropriately specific for this type of merge operation. While the title does not enumerate individual changes (version updates, UI modifications, network refactoring, localization additions), this level of detail is not typically expected in a backmerge title where the operation itself is the primary focus. The title is clear, concise, and directly related to what the changeset accomplishes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch commcare_2.60

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (2)
app/src/org/commcare/services/FCMMessageData.java (1)

116-131: Add serialization versioning and migration code to prevent data loss when deserializing persisted FCMMessageData objects.

The new action field added to writeExternal (line 127) creates a breaking change. FCMMessageData is persisted in SharedPreferences via HiddenPreferences.setPendingSyncRequest(), and old serialized data lacks the action field. When users upgrade and the app attempts to deserialize old data, readExternal at line 118 will read the wrong fields (attempting to deserialize username as the action field), causing data corruption or crashes in getPendingSyncRequest() which has no error handling.

Required fixes:

  1. Add a version field to readExternal/writeExternal (written first, checked first during deserialization)
  2. Implement conditional logic in readExternal to handle both old (4-field) and new (5-field) formats
  3. When reading old data, derive action from actionType or set a default value
app/src/org/commcare/connect/network/ConnectNetworkHelper.java (1)

143-146: Don’t log raw error bodies; fix typo and standardize logging.

Use NetworkUtils.logFailedResponse for consistency and to avoid dumping raw payloads.

-                } else if (response.errorBody() != null) {
-                    String error = response.errorBody().string();
-                    Logger.log("Netowrk Error", error);
-                }
+                } else if (response.errorBody() != null) {
+                    String error = response.errorBody().string();
+                    NetworkUtils.logFailedResponse(response.message(), responseCode, url, error);
+                }
♻️ Duplicate comments (3)
app/src/org/commcare/activities/InstallFromListActivity.java (1)

327-329: Same unused errorStream parameter pattern.

Like PostRequestActivity.java, the errorStream parameter is unused. This appears to be a consistent pattern across the error handling refactor.

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

176-178: Unused errorStream parameter (duplicate pattern).

Same observation as PostRequestActivity.java and InstallFromListActivity.java.

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

83-85: Unused errorStream parameter (duplicate pattern).

Consistent with the pattern in activity files.

🧹 Nitpick comments (10)
app/src/org/commcare/views/QuestionsView.java (1)

362-373: Helper method correctly builds diagnostic information.

The null handling and inclusion of parent class information will be helpful for debugging.

Minor formatting: For consistency, consider spacing around the concatenation operator at line 369:

-                focusedViewClassName += "/"+ focusedView.getParent().getClass();
+                focusedViewClassName += "/" + focusedView.getParent().getClass();
app/src/org/commcare/connect/database/JobStoreManager.java (1)

104-107: Good defensive state management.

This guard correctly prevents job status from regressing during updates, which is essential for maintaining state machine integrity. The logic assumes higher status values represent more advanced states, which is a standard pattern for status enumerations.

Consider adding logging when status downgrades are prevented to aid debugging:

 if(existingJob.getStatus() > job.getStatus()) {
     //Status should never go backwards
+    Logger.log(LogTypes.TYPE_MAINTENANCE, 
+        String.format("Prevented status downgrade for job %d: %d -> %d", 
+            job.getJobId(), existingJob.getStatus(), job.getStatus()));
     job.setStatus(existingJob.getStatus());
 }
app/src/org/commcare/services/FCMMessageData.java (1)

126-127: Consider serializing only action instead of both fields.

Since actionType is derived from action via the getActionType method (lines 100-113), serializing both fields is redundant. You could reduce the serialized data size by only writing action and reconstructing actionType during deserialization.

Apply this diff to remove the redundancy:

 @Override
 public void writeExternal(DataOutputStream out) throws IOException {
-    ExtUtil.writeString(out, actionType.toString());
     ExtUtil.writeString(out, action);
     ExtUtil.writeString(out, username);
     ExtUtil.writeString(out, domain);
     ExtUtil.writeNumeric(out, creationTime.getMillis());
 }

 @Override
 public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
-    actionType = ActionTypes.valueOf(ExtUtil.readString(in));
     action = ExtUtil.readString(in);
+    actionType = getActionType(action);
     username = ExtUtil.readString(in);
     domain = ExtUtil.readString(in);
     creationTime = new DateTime(ExtUtil.readLong(in));
 }
app/src/org/commcare/connect/network/base/BaseApi.kt (1)

43-45: Consider including exception details in the error body.

When stream reading fails, the error is logged but processFailure is called with an empty error body. Consider passing exception details to help with debugging:

 } catch (e: IOException) {
     Logger.exception("Error reading response stream", e);
-    callback.processFailure(response.code(), endPoint, "")
+    callback.processFailure(response.code(), endPoint, "Error reading response: ${e.message}")
 }
app/src/org/commcare/activities/connect/ConnectActivity.java (1)

145-147: Typo + clarity: rename local var and guard menu item lookup.

-        MenuItem notiificationsMenuItem = menu.findItem(R.id.action_bell);
-        notiificationsMenuItem.setVisible(PersonalIdFeatureFlagChecker.isFeatureEnabled(NOTIFICATIONS));
+        MenuItem notificationsMenuItem = menu.findItem(R.id.action_bell);
+        if (notificationsMenuItem != null) {
+            notificationsMenuItem.setVisible(PersonalIdFeatureFlagChecker.isFeatureEnabled(NOTIFICATIONS));
+        }
app/src/org/commcare/navdrawer/BaseDrawerController.kt (1)

238-248: Fix method name typo and references for readability.

-        binding.notificationView.visibility = if (shouldShowNotiifcations()) View.VISIBLE else View.GONE
+        binding.notificationView.visibility = if (shouldShowNotifications()) View.VISIBLE else View.GONE
@@
-    private fun shouldShowNotiifcations(): Boolean {
-        return PersonalIdManager.getInstance().isloggedIn() && isFeatureEnabled(NOTIFICATIONS);
+    private fun shouldShowNotifications(): Boolean {
+        return PersonalIdManager.getInstance().isloggedIn() && isFeatureEnabled(NOTIFICATIONS)
     }
app/src/org/commcare/connect/network/ConnectNetworkHelper.java (2)

359-371: Optional: log server/other errors via the same utility.

For parity, consider:

-                handler.processFailure(responseCode, url, "");
+                NetworkUtils.logFailedResponse("", responseCode, url, "");
+                handler.processFailure(responseCode, url, "");
@@
-                handler.processFailure(responseCode, url, "");
+                NetworkUtils.logFailedResponse("", responseCode, url, "");
+                handler.processFailure(responseCode, url, "");

311-313: Pass full GET URL to the processor for accurate logging.

Currently the base URL is passed; prefer getUrl.toString() so downstream logs show the full endpoint with query.

Example:

getTask.connect(getResponseProcessor(context, getUrl.toString(), authInfo instanceof AuthInfo.TokenAuth,
        background, handler));
app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java (1)

72-88: Minor: avoid writing empty failure codes to session.

-        try {
-            sessionData.setSessionFailureCode(errorCode);
+        try {
+            if (errorCode != null && !errorCode.isEmpty()) {
+                sessionData.setSessionFailureCode(errorCode);
+            }
app/src/org/commcare/connect/network/NetworkUtils.kt (1)

66-66: Remove unnecessary null-safety checks on non-nullable parameters.

The endPoint parameter is declared as non-nullable String in both methods, so the elvis operators on lines 66 and 85 (endPoint ?: "...") are unnecessary and misleading.

Apply this diff:

-    errorMessage += " for url ${endPoint ?: "unknown url"}"
+    errorMessage += " for url $endPoint"

And for line 85:

-    errorMessage += " for url ${endPoint ?: "url not found"}"
+    errorMessage += " for url $endPoint"

Also applies to: 85-85

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2689b7c and 41238d1.

📒 Files selected for processing (34)
  • app/AndroidManifest.xml (1 hunks)
  • app/res/menu/menu_connect.xml (0 hunks)
  • app/res/values-es/strings.xml (1 hunks)
  • app/res/values-fr/strings.xml (1 hunks)
  • app/res/values-hi/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/strings.xml (1 hunks)
  • app/src/org/commcare/activities/InstallFromListActivity.java (1 hunks)
  • app/src/org/commcare/activities/PostRequestActivity.java (1 hunks)
  • app/src/org/commcare/activities/QueryRequestActivity.java (1 hunks)
  • app/src/org/commcare/activities/connect/ConnectActivity.java (5 hunks)
  • app/src/org/commcare/adapters/EntityStringFilterer.java (1 hunks)
  • app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java (1 hunks)
  • app/src/org/commcare/connect/MessageManager.java (5 hunks)
  • app/src/org/commcare/connect/database/JobStoreManager.java (1 hunks)
  • app/src/org/commcare/connect/network/ConnectNetworkHelper.java (7 hunks)
  • app/src/org/commcare/connect/network/IApiCallback.java (1 hunks)
  • app/src/org/commcare/connect/network/NetworkUtils.kt (1 hunks)
  • app/src/org/commcare/connect/network/base/BaseApi.kt (3 hunks)
  • app/src/org/commcare/connect/network/base/BaseApiCallback.kt (2 hunks)
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java (4 hunks)
  • app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java (1 hunks)
  • app/src/org/commcare/fragments/personalId/PersonalIdBiometricConfigFragment.java (1 hunks)
  • app/src/org/commcare/navdrawer/BaseDrawerController.kt (2 hunks)
  • app/src/org/commcare/network/GetAndParseActor.java (1 hunks)
  • app/src/org/commcare/personalId/PersonalIdFeatureFlagChecker.kt (2 hunks)
  • app/src/org/commcare/services/FCMMessageData.java (1 hunks)
  • app/src/org/commcare/tasks/DataPullTask.java (2 hunks)
  • app/src/org/commcare/tasks/EntityLoaderTask.java (1 hunks)
  • app/src/org/commcare/tasks/FormLoaderTask.java (1 hunks)
  • app/src/org/commcare/tasks/ModernHttpTask.java (3 hunks)
  • app/src/org/commcare/views/QuestionsView.java (2 hunks)
💤 Files with no reviewable changes (1)
  • app/res/menu/menu_connect.xml
🧰 Additional context used
🧠 Learnings (3)
📓 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.
Learnt from: Jignesh-dimagi
PR: dimagi/commcare-android#0
File: :0-0
Timestamp: 2025-05-08T13:40:19.645Z
Learning: PR #3048 introduces a comprehensive messaging system in the Connect feature, implementing secure encryption using AES-GCM for message content, proper channel management with consent flows, and a well-designed UI separation between sent and received messages with real-time notification integration.
📚 Learning: 2025-05-22T14:32:53.133Z
Learnt from: OrangeAndGreen
PR: dimagi/commcare-android#3121
File: app/res/values-ti/strings.xml:350-350
Timestamp: 2025-05-22T14:32:53.133Z
Learning: PersonalID and Connect features haven't been translated to Spanish, Lithuanian, or Norwegian yet, so users with those language settings see the English strings by default.

Applied to files:

  • app/res/values-es/strings.xml
📚 Learning: 2025-05-08T11:08:18.530Z
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.

Applied to files:

  • app/src/org/commcare/activities/connect/ConnectActivity.java
🧬 Code graph analysis (9)
app/src/org/commcare/navdrawer/BaseDrawerController.kt (1)
app/src/org/commcare/personalId/PersonalIdFeatureFlagChecker.kt (1)
  • isFeatureEnabled (21-28)
app/src/org/commcare/connect/MessageManager.java (1)
app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)
  • processFailure (20-53)
app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java (2)
app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java (1)
  • FirebaseAnalyticsUtil (43-608)
app/src/org/commcare/google/services/analytics/AnalyticsParamValue.java (1)
  • AnalyticsParamValue (7-194)
app/src/org/commcare/connect/network/IApiCallback.java (1)
app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)
  • processFailure (20-53)
app/src/org/commcare/activities/connect/ConnectActivity.java (3)
app/src/org/commcare/connect/ConnectConstants.java (1)
  • ConnectConstants (8-49)
app/src/org/commcare/connect/database/ConnectJobUtils.java (1)
  • ConnectJobUtils (25-383)
app/src/org/commcare/personalId/PersonalIdFeatureFlagChecker.kt (1)
  • isFeatureEnabled (21-28)
app/src/org/commcare/fragments/personalId/PersonalIdBiometricConfigFragment.java (1)
app/src/org/commcare/utils/BiometricsHelper.java (1)
  • BiometricsHelper (33-258)
app/src/org/commcare/connect/network/base/BaseApi.kt (1)
app/src/org/commcare/connect/network/NetworkUtils.kt (1)
  • logFailedResponse (49-73)
app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java (2)
app/src/org/commcare/connect/network/NetworkUtils.kt (1)
  • getErrorCodes (35-47)
app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)
  • processFailure (20-53)
app/src/org/commcare/connect/network/ConnectNetworkHelper.java (2)
app/src/org/commcare/connect/network/NetworkUtils.kt (2)
  • getErrorBody (15-26)
  • logFailedResponse (49-73)
app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)
  • processFailure (20-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Lint Code Base
🔇 Additional comments (27)
app/AndroidManifest.xml (1)

5-5: Verify the version downgrade before merging.

The versionName is being downgraded from "2.61" to "2.60.0" while versionCode remains at "106". This version downgrade is unusual and could introduce issues:

  • App store systems may reject or incorrectly handle version downgrades.
  • User upgrade paths may break if master is the active development branch.
  • The mismatch between versionCode (106) and versionName (2.60.0) could confuse version tracking.

Confirm this is an intentional backmerge of hotfixes and ensure the version management strategy is documented. Consider whether versionCode should also be adjusted for consistency with the 2.60 release timeline.

app/src/org/commcare/adapters/EntityStringFilterer.java (1)

77-79: LGTM! Exception logging improves observability.

This change properly surfaces tracing errors for debugging without crashing the filter operation. The logging approach is appropriate for performance monitoring code where failures should be visible but non-blocking.

app/src/org/commcare/tasks/EntityLoaderTask.java (1)

72-74: Good improvement to error visibility.

Logging the exception when tracing fails to stop is a solid observability improvement. This helps diagnose potential Firebase performance monitoring issues without affecting the primary entity loading flow.

app/src/org/commcare/tasks/DataPullTask.java (2)

285-287: Good improvement to error visibility.

Logging exceptions when stopping performance tracing helps surface monitoring issues without disrupting the sync flow.


335-337: Consistent error handling.

This mirrors the pattern at lines 285-287, ensuring tracing stop failures are logged in both success and failure sync scenarios.

app/src/org/commcare/tasks/FormLoaderTask.java (1)

157-159: Good improvement to error visibility.

Consistent with the error handling pattern applied to EntityLoaderTask and DataPullTask, this improves observability of Firebase performance monitoring issues during form loading.

app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java (1)

259-266: LGTM! Analytics tracking added for account lockout.

The addition of reportPersonalIdConfigurationFailure properly tracks account lockout failures during the backup code recovery flow. The placement after logRecoveryResult(false) and before UI updates is appropriate, and the constant START_CONFIGURATION_LOCKED_ACCOUNT_FAILURE accurately represents the failure scenario.

app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java (1)

566-576: Verify the behavioral change aligns with business requirements.

The logic now counts all deliveries dated today regardless of their status. This means users may be blocked from submitting additional deliveries if they've reached their daily maximum, even if some submissions are pending approval or were rejected.

This change makes the method consistent with getDeliveryCountsPerPaymentUnit() (lines 587-602), which also doesn't filter by status.

Please confirm this behavioral change is intentional. Should daily visit limits apply to all delivery attempts, or only to approved deliveries?

app/src/org/commcare/views/QuestionsView.java (2)

43-43: LGTM!

Import is required for the @NonNull annotation used in the new helper method below.


351-360: Good defensive logging for diagnosing resize failures.

The try-catch pattern captures valuable context (dimensions and focused view) before the crash occurs. Note that since the exception is rethrown, the crash will still happen—this code adds diagnostic information to help identify the root cause.

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

346-348: LGTM! Localization strings properly support conditional unlock method messaging.

The capitalization fix to "PersonalID" ensures consistency, and the two new strings (connect_verify_message_pin and connect_verify_message_fingerprint) enable the UI to display context-appropriate guidance based on available unlock methods.

app/src/org/commcare/fragments/personalId/PersonalIdBiometricConfigFragment.java (1)

133-174: Solid refactoring to support conditional unlock method UI.

The two-phase approach (determine UI state → bind title/message) is well-designed:

  1. Lines 137-153 determine which unlock options are available and set appropriate messages
  2. Lines 161-170 compute the title based on visible buttons
  3. Lines 172-173 bind the final title and message

The logic correctly handles all combinations of fingerprint hardware presence, PIN requirements, and biometric requirements, using the new localized strings appropriately.

app/src/org/commcare/tasks/ModernHttpTask.java (3)

37-37: LGTM! Consistent field addition.

The new errorResponseDataStream field mirrors the existing responseDataStream pattern and supports exposing error response bodies.


100-105: LGTM! Proper nullability handling.

The @Nullable annotation correctly indicates that this method returns null for successful responses. Consumers of this API must handle the nullable return appropriately.


73-75: Fix extra whitespace before opening brace on line 73 and verify stream cleanup.

The extra space in } else { (line 73) should be corrected to } else { per Java style conventions.

Regarding resource management: The responseDataStream and errorResponseDataStream are captured at lines 72 and 74, then passed to ModernHttpRequester.processResponse() via a ResponseStreamAccessor. The implementation of processResponse() and how it manages these streams could not be located in the searchable source. Confirm that all code paths in the HTTP response processing chain properly close both success and error response streams to prevent resource leaks.

app/src/org/commcare/personalId/PersonalIdFeatureFlagChecker.kt (2)

15-15: LGTM! Infrastructure for NOTIFICATIONS feature flag.

The constant is properly defined and integrated into the feature flag check logic.


25-25: LGTM! Feature flag returns false (disabled by default).

Appropriate default for a new feature being introduced via backmerge.

app/src/org/commcare/connect/network/IApiCallback.java (2)

12-12: LGTM! Error handling signature improved.

The refactored signature moves to string-based error bodies and reorders parameters for better consistency (url before errorBody). This change is properly reflected across all implementations in the codebase.


16-16: LGTM! New callback method added.

processTokenRequestDeniedError() provides specific handling for token denial scenarios, improving error handling granularity.

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

90-90: LGTM! All processFailure calls properly migrated.

All five anonymous IApiCallback implementations consistently use the new signature (responseCode, url, errorBody), aligning with the IApiCallback interface changes.

Also applies to: 142-142, 194-194, 254-254, 308-308

app/src/org/commcare/connect/network/base/BaseApi.kt (1)

50-56: LGTM! Proper error handling with NetworkUtils.

The refactoring correctly:

  • Extracts error body using NetworkUtils
  • Logs failure details consistently
  • Ensures stream cleanup in the finally block
app/src/org/commcare/activities/PostRequestActivity.java (1)

182-196: Clarify that errorStream is part of the external interface contract and review the suggestion for optional logging.

The errorStream parameter is indeed unused in this method. However, it's part of the HttpResponseProcessor interface signature from the external org.commcare.core library, which all implementations must follow. The same parameter goes unused across all five implementations in the codebase (GetAndParseActor, QueryRequestActivity, PostRequestActivity, InstallFromListActivity, and ConnectNetworkHelper).

If you want to leverage the error stream for enhanced debugging, the suggestion to extract and log diagnostic information is reasonable but optional. Otherwise, this can be left as-is since it's a required interface method signature.

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

329-348: Good: client error now extracts and forwards errorBody.

Error stream parsing + structured logging + callback propagation look correct.

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

62-67: Error-body routing looks correct.

Parses errorBody once and delegates to specific handlers; falls back to base handling when unknown.

app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)

20-53: API alignment confirmed—all implementations and call sites use the 3-arg form.

Verification shows the processFailure method signature is consistently implemented across all classes (BaseApiCallback, MessageManager, PersonalIdApiHandler) and all call sites pass exactly three arguments (responseCode, url, errorBody) in the correct positions. The interface contract in IApiCallback.java is properly defined and followed throughout the codebase.

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

100-109: Review comment is incorrect and contradicts the team's established fail-fast philosophy.

The review suggests adding defensive try-catch and null-checks, but the retrieved learnings from prior Connect feature PRs explicitly state the team prefers to let code crash for data contract violations.

Learning 2 directly addresses this: "opportunityId values are expected to always contain valid integer strings. Integer.parseInt() should be allowed to throw NumberFormatException if invalid data is encountered, following the fail-fast philosophy"

The current code at line 113 already follows this philosophy with Objects.requireNonNull(job), and PaymentAcknowledgeReceiver.java line 35 demonstrates the same pattern elsewhere: Integer.parseInt(opportunityId) without try-catch.

The code as-is correctly enforces data contracts and allows failures to propagate immediately.

Likely an incorrect or invalid review comment.

app/src/org/commcare/connect/network/NetworkUtils.kt (1)

76-91: Good error categorization approach.

The method appropriately categorizes network errors by throwable type and logs them consistently. The logging pattern on line 90 is correct (unlike line 72 in the previous method).

Comment on lines +212 to +214
<string name="connect_verify_message">Configura un método a continuación para desbloquear tanto tu dispositivo como tu cuenta de PersonalID</string>
<string name="connect_verify_message_pin">Configure un PIN o patrón a continuación para desbloquear su dispositivo y su cuenta PersonalID</string>
<string name="connect_verify_message_fingerprint">Configure su huella digital a continuación para desbloquear tanto su dispositivo como su cuenta PersonalID</string>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align imperative tone in Spanish localization.

Most Spanish strings in this screen use the informal imperative (“Configura”), but Lines 213-214 switch to the formal “Configure,” breaking consistency. Please keep all three sentences in the same voice (e.g., “Configura…”).

-    <string name="connect_verify_message_pin">Configure un PIN o patrón a continuación para desbloquear su dispositivo y su cuenta PersonalID</string>
-    <string name="connect_verify_message_fingerprint">Configure su huella digital a continuación para desbloquear tanto su dispositivo como su cuenta PersonalID</string>
+    <string name="connect_verify_message_pin">Configura un PIN o patrón a continuación para desbloquear tu dispositivo y tu cuenta PersonalID</string>
+    <string name="connect_verify_message_fingerprint">Configura tu huella digital a continuación para desbloquear tanto tu dispositivo como tu cuenta PersonalID</string>
🤖 Prompt for AI Agents
In app/res/values-es/strings.xml around lines 212 to 214, the three strings on
this screen mix informal ("Configura") and formal ("Configure") imperatives;
change the two formal occurrences to the informal form to match the rest of the
UI. Update the values for connect_verify_message_pin and
connect_verify_message_fingerprint to use "Configura" and keep the rest of the
phrasing identical (i.e., "Configura un PIN o patrón a continuación..." and
"Configura su huella digital a continuación...") so all three strings share the
same informal imperative tone.

Comment on lines +16 to +26
fun getErrorBody(stream: InputStream?): String {
try {
if (stream != null) {
val errorBytes = StreamsUtil.inputStreamToByteArray(stream)
return String(errorBytes, StandardCharsets.UTF_8)
}
} catch (e: Exception) {
Logger.exception("Error parsing error_code", e);
}
return ""
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Resource leak - stream is never closed.

The InputStream is read but never closed, which will leak file descriptors and potentially exhaust system resources over repeated failures.

Apply this diff to close the stream in a finally block:

 @JvmStatic
 fun getErrorBody(stream: InputStream?): String {
+    var errorBytes: ByteArray? = null
     try {
         if (stream != null) {
-            val errorBytes = StreamsUtil.inputStreamToByteArray(stream)
+            errorBytes = StreamsUtil.inputStreamToByteArray(stream)
-            return String(errorBytes, StandardCharsets.UTF_8)
         }
     } catch (e: Exception) {
         Logger.exception("Error parsing error_code", e);
+    } finally {
+        StreamsUtil.closeStream(stream)
     }
-    return ""
+    return if (errorBytes != null) String(errorBytes, StandardCharsets.UTF_8) else ""
 }
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/network/NetworkUtils.kt around lines 16 to 26,
the InputStream passed to getErrorBody is read but never closed causing a
resource leak; update the function to ensure the stream is closed after reading
— either wrap the read in Kotlin's use { } block on the InputStream or add a
try/finally that closes stream in the finally block (safely checking for null
and swallowing/ logging any IOException on close) so the bytes are returned as
before but the InputStream is always closed.

Comment on lines +22 to +23
} catch (e: Exception) {
Logger.exception("Error parsing error_code", 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 | 🟠 Major

Misleading exception message and overly broad exception handling.

The exception message says "Error parsing error_code" but this method reads the error body stream, not parses error codes. Additionally, catching generic Exception is too broad and may hide specific I/O failures.

Apply this diff:

-    } catch (e: Exception) {
-        Logger.exception("Error parsing error_code", e);
+    } catch (e: IOException) {
+        Logger.exception("Error reading error response body", 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 (e: Exception) {
Logger.exception("Error parsing error_code", e);
} catch (e: IOException) {
Logger.exception("Error reading error response body", e);
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/network/NetworkUtils.kt around lines 22-23,
replace the overly broad catch of Exception and the misleading message "Error
parsing error_code" with a catch for the specific I/O exception thrown when
reading the error body (e.g., IOException). Update the Logger call to a correct
message such as "Error reading error body" and log the caught IOException; do
not swallow other exceptions—either rethrow them or allow them to propagate so
higher layers can handle them.

Comment on lines +28 to +47
/**
* Extracts error_code and error_sub_code from a JSON error response body.
* If the stream is null or parsing fails, returns empty strings for both codes.
*
* @param stream InputStream of the error response body
* @return Pair of error_code and error_sub_code
*/
@JvmStatic
fun getErrorCodes(errorBody: String): Pair<String, String> {
var errorCode = ""
var errorSubCode = ""
try {
val json = JSONObject(errorBody)
errorCode = json.optString("error_code", "");
errorSubCode = json.optString("error_sub_code", "");
} catch (e: Exception) {
Logger.exception("Error parsing error_code", e);
}
return Pair(errorCode, errorSubCode)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix incorrect KDoc parameter and refine exception handling.

The KDoc at line 32 references @param stream InputStream but the actual parameter is errorBody: String. Additionally, catching generic Exception should be narrowed to JSONException for clarity.

Apply this diff:

 /**
  * Extracts error_code and error_sub_code from a JSON error response body.
  * If the stream is null or parsing fails, returns empty strings for both codes.
  *
- * @param stream InputStream of the error response body
+ * @param errorBody String containing the JSON error response body
  * @return Pair of error_code and error_sub_code
  */
 @JvmStatic
 fun getErrorCodes(errorBody: String): Pair<String, String> {
     var errorCode = ""
     var errorSubCode = ""
     try {
         val json = JSONObject(errorBody)
         errorCode = json.optString("error_code", "");
         errorSubCode = json.optString("error_sub_code", "");
-    } catch (e: Exception) {
+    } catch (e: org.json.JSONException) {
         Logger.exception("Error parsing error_code", e);
     }
     return Pair(errorCode, errorSubCode)
 }
📝 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
/**
* Extracts error_code and error_sub_code from a JSON error response body.
* If the stream is null or parsing fails, returns empty strings for both codes.
*
* @param stream InputStream of the error response body
* @return Pair of error_code and error_sub_code
*/
@JvmStatic
fun getErrorCodes(errorBody: String): Pair<String, String> {
var errorCode = ""
var errorSubCode = ""
try {
val json = JSONObject(errorBody)
errorCode = json.optString("error_code", "");
errorSubCode = json.optString("error_sub_code", "");
} catch (e: Exception) {
Logger.exception("Error parsing error_code", e);
}
return Pair(errorCode, errorSubCode)
}
/**
* Extracts error_code and error_sub_code from a JSON error response body.
* If the stream is null or parsing fails, returns empty strings for both codes.
*
* @param errorBody String containing the JSON error response body
* @return Pair of error_code and error_sub_code
*/
@JvmStatic
fun getErrorCodes(errorBody: String): Pair<String, String> {
var errorCode = ""
var errorSubCode = ""
try {
val json = JSONObject(errorBody)
errorCode = json.optString("error_code", "");
errorSubCode = json.optString("error_sub_code", "");
} catch (e: org.json.JSONException) {
Logger.exception("Error parsing error_code", e);
}
return Pair(errorCode, errorSubCode)
}
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/network/NetworkUtils.kt around lines 28 to 47,
the KDoc incorrectly documents a non-existent InputStream parameter and the
catch is too broad; update the KDoc to describe the actual parameter errorBody:
String (replace the @param stream line with @param errorBody the JSON error
response as String) and narrow the exception catch to catch
org.json.JSONException (import if necessary) so only JSON parse errors are
handled; keep returning Pair(errorCode, errorSubCode).

Comment on lines +68 to +72
Logger.log(
LogTypes.TYPE_ERROR_SERVER_COMMS,
errorMessage
)
Logger.exception(LogTypes.TYPE_ERROR_SERVER_COMMS, Throwable(errorMessage))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Incorrect Logger.exception signature on line 72.

Line 72 calls Logger.exception(LogTypes.TYPE_ERROR_SERVER_COMMS, Throwable(errorMessage)), which appears to pass a log type constant as the first parameter. Comparing with line 90 (Logger.exception(errorMessage, t)), the signature should be Logger.exception(message: String, throwable: Throwable).

Apply this diff to fix the call:

 Logger.log(
     LogTypes.TYPE_ERROR_SERVER_COMMS,
     errorMessage
 )
-Logger.exception(LogTypes.TYPE_ERROR_SERVER_COMMS, Throwable(errorMessage))
+Logger.exception(errorMessage, Throwable(errorMessage))
📝 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
Logger.log(
LogTypes.TYPE_ERROR_SERVER_COMMS,
errorMessage
)
Logger.exception(LogTypes.TYPE_ERROR_SERVER_COMMS, Throwable(errorMessage))
Logger.log(
LogTypes.TYPE_ERROR_SERVER_COMMS,
errorMessage
)
Logger.exception(errorMessage, Throwable(errorMessage))
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/network/NetworkUtils.kt around lines 68-72, the
Logger.exception call uses the wrong parameter order; change it to call
Logger.exception(message: String, throwable: Throwable). Replace the existing
call so the first argument is errorMessage and the second is the throwable
(e.g., Logger.exception(errorMessage, t) if a caught exception variable t
exists, otherwise Logger.exception(errorMessage, Throwable(errorMessage))).

Comment on lines 222 to 234
private fun updateNotificationsIcon() {
if(shouldShowNotiifcations()) {
val notifications = NotificationRecordDatabaseHelper().getAllNotifications(activity)
val hasUnreadNotification = notifications!!.any { !it.readStatus }

binding.ivNotification.setImageResource(
if (hasUnreadNotification) R.drawable.ic_new_notification_bell
else R.drawable.ic_bell
)
}
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Unread bell never updates: helper is never invoked.

updateNotificationsIcon() isn’t called; the bell stays at default.

Call it after populating the signed‑in UI (e.g., end of the true branch in refreshDrawerContent):

// after navDrawerAdapter.refreshList(items)
updateNotificationsIcon()

Search for any existing calls:


🏁 Script executed:

#!/bin/bash
rg -n 'updateNotificationsIcon\('

Length of output: 102


I need to verify the broader context and confirm the suggested fix location is appropriate.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find refreshDrawerContent definition and surrounding context
rg -n -A15 'fun refreshDrawerContent\(' app/src/org/commcare/navdrawer/BaseDrawerController.kt

Length of output: 799


🏁 Script executed:

#!/bin/bash
# Search for any notification-related updates or listeners in the file
rg -n 'notification|Notification' app/src/org/commcare/navdrawer/BaseDrawerController.kt -i

Length of output: 946


🏁 Script executed:

#!/bin/bash
# Find navDrawerAdapter.refreshList call location in refreshDrawerContent
rg -n 'navDrawerAdapter\.refreshList' app/src/org/commcare/navdrawer/BaseDrawerController.kt

Length of output: 51


Add missing call to updateNotificationsIcon() in refreshDrawerContent().

The function updateNotificationsIcon() is properly implemented but never invoked, leaving the notification bell in its default state. Add the call after navDrawerAdapter.refreshList(items) on line 216:

navDrawerAdapter.refreshList(items)
updateNotificationsIcon()
🤖 Prompt for AI Agents
In app/src/org/commcare/navdrawer/BaseDrawerController.kt around lines 222 to
233, updateNotificationsIcon() is implemented but never called; modify
refreshDrawerContent() to invoke updateNotificationsIcon() immediately after the
navDrawerAdapter.refreshList(items) call (i.e., insert a call to
updateNotificationsIcon() on the line following
navDrawerAdapter.refreshList(items)) so the notification bell updates when the
drawer content is refreshed.

Comment on lines 224 to 232
val notifications = NotificationRecordDatabaseHelper().getAllNotifications(activity)
val hasUnreadNotification = notifications!!.any { !it.readStatus }

binding.ivNotification.setImageResource(
if (hasUnreadNotification) R.drawable.ic_new_notification_bell
else R.drawable.ic_bell
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Avoid NPE on notifications list; use safe check.

-        if(shouldShowNotiifcations()) {
-            val notifications = NotificationRecordDatabaseHelper().getAllNotifications(activity)
-            val hasUnreadNotification = notifications!!.any { !it.readStatus }
+        if (shouldShowNotifications()) {
+            val notifications = NotificationRecordDatabaseHelper().getAllNotifications(activity)
+            val hasUnreadNotification = notifications?.any { !it.readStatus } == true

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

🤖 Prompt for AI Agents
In app/src/org/commcare/navdrawer/BaseDrawerController.kt around lines 224 to
231, the code assumes
NotificationRecordDatabaseHelper().getAllNotifications(activity) returns
non-null and uses !! which can cause an NPE; update the call to use a null-safe
pattern (e.g., val notifications =
NotificationRecordDatabaseHelper().getAllNotifications(activity) ?: emptyList())
and then compute hasUnreadNotification with notifications.any { !it.readStatus }
so the null case is treated as empty and no force-unwrapping is used; set the
notification icon based on that result.

@coderabbitai
Copy link

coderabbitai bot commented Oct 20, 2025

📝 Walkthrough

Walkthrough

This PR implements multiple interconnected updates to the CommCare Android application, including a version downgrade (2.61 → 2.60.0), removal of the messaging menu item from ConnectActivity, and addition of PIN and fingerprint authentication verification message strings across multiple locales (Spanish, French, Hindi, Portuguese, Swahili, Tigrinya, and English). The PR refactors network error-handling infrastructure by updating method signatures across IApiCallback, BaseApi, and related classes to propagate error bodies as strings rather than InputStreams. It introduces a new NetworkUtils singleton for centralized error body parsing and logging. UI logic for PersonalIdBiometricConfigFragment is consolidated to streamline fingerprint/PIN option determination. A NOTIFICATIONS feature flag is added to PersonalIdFeatureFlagChecker and integrated into ConnectActivity and BaseDrawerController. Job status preservation logic prevents status regression during updates. Exception logging is enhanced in multiple task classes, and FCMMessageData serialization is corrected to persist both actionType and action fields.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

This PR spans 30+ files with heterogeneous changes including method signature updates across multiple API layers, significant UI refactoring in PersonalIdBiometricConfigFragment, new networking infrastructure (NetworkUtils), feature flag integration, and localization updates. The changes are not repetitive but require separate reasoning for error-handling flow updates, UI logic consolidation, and feature-flag propagation across multiple components.

Possibly related PRs

Suggested labels

skip-integration-tests, QA Note

Suggested reviewers

  • Jignesh-dimagi
  • shubham1g5

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description is significantly incomplete relative to the provided template. The author has only provided a single line under "Product Description" ("Backmerge Commcare 2.60.0") and omitted all other required sections, including Technical Summary (with ticket/document link and design rationale), Feature Flag, Safety Assurance (safety story, automated test coverage, QA plan), and the Labels and Review checklist. Given the scope and complexity of this backmerge—which includes version changes, UI modifications, method signature changes across multiple files, network error handling refactoring, feature flag additions, and localization updates—the absence of these critical sections leaves reviewers without essential context for evaluating safety, test coverage, and QA readiness. The author should expand the pull request description to include all required template sections: provide a technical summary with links to relevant tickets or design documents explaining the backmerge rationale and key changes, identify any relevant feature flags (particularly the new NOTIFICATIONS flag), detail the safety story and testing approach, describe the QA plan and link to related QA tickets, and complete the review checklist regarding QA notes, release notes, risk labels, and reviewer appropriateness for this significant backmerge.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Backmerge Commcare 2.60.0" clearly and specifically describes the main change in the pull request. Based on the PR objectives, this is indeed a backmerge of CommCare 2.60.0 from the commcare_2.60 branch into master. The title accurately conveys the primary action and version number, making it clear to reviewers scanning the history what this PR accomplishes. The phrasing is concise, readable, and avoids vague terminology.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch commcare_2.60

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (6)
app/src/org/commcare/connect/network/base/BaseApi.kt (1)

39-46: Also log the success-body read failure.

If reading the success body fails, log as a failed response for parity with non-2xx errors before delegating to processFailure.

Apply this diff:

} catch (e: IOException) {
-    Logger.exception("Error reading response stream", e);
-    // Handle error when reading the stream
-    callback.processFailure(response.code(), endPoint, "")
+    Logger.exception("Error reading response stream", e)
+    // Log as failed response for consistency
+    logFailedResponse("Error reading response stream", response.code(), endPoint, "")
+    callback.processFailure(response.code(), endPoint, "")
}
app/src/org/commcare/activities/connect/ConnectActivity.java (1)

74-82: Guard against missing job when GO_TO_JOB_STATUS is true.

handleInfoRedirect() requires a non-null job; add a fallback to avoid NPE if extras didn’t resolve a job.

Apply this diff:

-        if (getIntent().getBooleanExtra(GO_TO_JOB_STATUS, false)) {
-            startDestinationId = handleInfoRedirect(startArgs);
+        if (getIntent().getBooleanExtra(GO_TO_JOB_STATUS, false)) {
+            if (job == null) {
+                org.javarosa.core.services.Logger.exception(
+                        "GO_TO_JOB_STATUS set but no job resolved from extras",
+                        new Exception("Missing job"));
+                return startDestinationId;
+            }
+            startDestinationId = handleInfoRedirect(startArgs);
app/src/org/commcare/connect/network/ConnectNetworkHelper.java (2)

329-349: Close errorStream to prevent resource leaks.

errorStream is read but never closed; this can exhaust connections/file descriptors under load.

Apply this diff:

-                    } else {
-                        String errorBody = NetworkUtils.getErrorBody(errorStream);
-                        NetworkUtils.logFailedResponse("", responseCode, url, errorBody);
-                        handler.processFailure(responseCode, url, errorBody);
-                    }
+                    } else {
+                        String errorBody = "";
+                        try {
+                            errorBody = NetworkUtils.getErrorBody(errorStream);
+                        } finally {
+                            StreamsUtil.closeStream(errorStream);
+                        }
+                        NetworkUtils.logFailedResponse("", responseCode, url, errorBody);
+                        handler.processFailure(responseCode, url, errorBody);
+                    }

192-213: Bug: headers map not populated for form-encoded POST.

Reassigning outputHeaders breaks propagation to the caller; version/content headers are lost.

Apply this diff:

         if (useFormEncoding) {
             Multimap<String, String> multimap = ArrayListMultimap.create();
             for (Map.Entry<String, Object> entry : params.entrySet()) {
                 multimap.put(entry.getKey(), entry.getValue().toString());
             }

             requestBody = ModernHttpRequester.getPostBody(multimap);
-            outputHeaders = getContentHeadersForXFormPost(requestBody);
+            HashMap<String, String> contentHeaders = getContentHeadersForXFormPost(requestBody);
+            outputHeaders.putAll(contentHeaders);
         } else {
app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java (2)

43-58: Fix double-callback and stream leak in processSuccess; use UTF‑8; crash on JSON contract breaches

  • onSuccess is called even after parse failures (after onFailure) → double callback.
  • When parser is null, responseData isn’t closed → resource leak.
  • Use UTF‑8 when constructing the JSON string.
  • Per team convention, JSONException should crash (contract bug), not be handled gracefully. Based on learnings.
@@
-            public void processSuccess(int responseCode, InputStream responseData) {
-                if (parser != null) {
-                    try (InputStream in = responseData) {
-                        JSONObject json = new JSONObject(new String(StreamsUtil.inputStreamToByteArray(in)));
-                        parser.parse(json, sessionData);
-                    } catch (JSONException e) {
-                        Logger.exception("JSON error parsing API response", e);
-                        onFailure(PersonalIdOrConnectApiErrorCodes.JSON_PARSING_ERROR, e);
-                    } catch (IOException e) {
-                        Logger.exception("Error parsing API response", e);
-                        onFailure(PersonalIdOrConnectApiErrorCodes.NETWORK_ERROR, e);
-                    }
-                }
-                onSuccess((T)sessionData);
-            }
+            public void processSuccess(int responseCode, InputStream responseData) {
+                try (InputStream in = responseData) {
+                    if (parser != null) {
+                        JSONObject json = new JSONObject(
+                                new String(StreamsUtil.inputStreamToByteArray(in), java.nio.charset.StandardCharsets.UTF_8));
+                        parser.parse(json, sessionData);
+                    }
+                    onSuccess((T) sessionData);
+                } catch (JSONException e) {
+                    Logger.exception("JSON error parsing API response", e);
+                    throw new RuntimeException("JSON contract violation parsing API response", e);
+                } catch (IOException e) {
+                    Logger.exception("Error parsing API response", e);
+                    onFailure(PersonalIdOrConnectApiErrorCodes.NETWORK_ERROR, e);
+                }
+            }

Also add:

+import java.nio.charset.StandardCharsets;

(Based on learnings)


90-97: Add createCallback overload for parser-first argument pattern

The four call sites at lines 96, 136, 151, and 156 pass arguments in reversed order relative to the existing method signature. The current createCallback(PersonalIdSessionData, PersonalIdApiResponseParser) method expects session data first, but these calls provide the parser first.

Adding an overload createCallback(PersonalIdApiResponseParser<T>, T) resolves the mismatch and provides proper handling for non-session flows:

   private IApiCallback createCallback(PersonalIdSessionData sessionData,
                                       PersonalIdApiResponseParser parser) {
     return new BaseApiCallback<T>(this) {
       ...
     };
   }
+
+  // Overload for non-session flows (e.g., ConnectUserRecord, requestId, or null).
+  private IApiCallback createCallback(final PersonalIdApiResponseParser<T> parser, final T data) {
+      return new BaseApiCallback<T>(this) {
+          @Override
+          public void processSuccess(int responseCode, InputStream responseData) {
+              try (InputStream in = responseData) {
+                  if (parser != null) {
+                      JSONObject json = new JSONObject(
+                              new String(StreamsUtil.inputStreamToByteArray(in), java.nio.charset.StandardCharsets.UTF_8));
+                      parser.parse(json, data);
+                  }
+                  onSuccess(data);
+              } catch (JSONException e) {
+                  Logger.exception("JSON error parsing API response", e);
+                  throw new RuntimeException("JSON contract violation parsing API response", e);
+              } catch (IOException e) {
+                  Logger.exception("Error parsing API response", e);
+                  onFailure(PersonalIdOrConnectApiErrorCodes.NETWORK_ERROR, e);
+              }
+          }
+
+          @Override
+          public void processFailure(int responseCode, String url, String errorBody) {
+              // No session-aware error-code handling for generic flows
+              super.processFailure(responseCode, url, errorBody);
+          }
+      };
+  }
🧹 Nitpick comments (10)
app/src/org/commcare/adapters/EntityStringFilterer.java (1)

77-79: Good addition of exception logging for observability.

This change improves debugging by surfacing errors that were previously silently ignored when stopping Firebase Performance Monitoring traces. Since tracing is a non-critical monitoring concern, catching and logging the exception while allowing execution to continue is appropriate.

Optional formatting nit: The log message has a trailing space ("Failed to stop tracing "). Consider removing it for consistency:

-                Logger.exception("Failed to stop tracing ", e);
+                Logger.exception("Failed to stop tracing", e);
app/src/org/commcare/views/QuestionsView.java (1)

362-373: Use consistent approach for class name extraction.

Line 367 explicitly calls .toString() on the class object, but line 369 relies on implicit toString() via string concatenation. For consistency and clarity, use the same approach for both.

Additionally, line 369 has inconsistent spacing in the concatenation operator.

Apply this diff for consistency:

     @NonNull
     private String getFocusedViewClassName() {
         View focusedView = findFocus();
         String focusedViewClassName = "None";
         if (focusedView != null) {
             focusedViewClassName = focusedView.getClass().toString();
             if (focusedView.getParent() != null) {
-                focusedViewClassName += "/"+ focusedView.getParent().getClass();
+                focusedViewClassName += "/" + focusedView.getParent().getClass().toString();
             }
         }
         return focusedViewClassName;
     }
app/src/org/commcare/connect/database/JobStoreManager.java (1)

104-107: Add logging when status is preserved to prevent regression.

The status preservation logic is a good defensive measure, but it operates silently. If the server sends a lower status value, this could indicate a data inconsistency or bug that should be investigated.

Apply this diff to add observability:

 if(existingJob.getStatus() > job.getStatus()) {
     //Status should never go backwards
+    Logger.log("JobStoreManager", "Preventing status downgrade for job " + 
+               job.getJobId() + " from " + existingJob.getStatus() + 
+               " to " + job.getStatus());
     job.setStatus(existingJob.getStatus());
 }
app/src/org/commcare/tasks/ModernHttpTask.java (2)

37-37: Stream ownership/closure: avoid leaked connections

Holding another InputStream increases the risk of not closing ResponseBody/errorBody. Please clarify ownership and close both success/error streams (and bodies) after processing to prevent OkHttp connection leaks.

Example patch (only if not already closed downstream):

@@
-            ModernHttpRequester.processResponse(
+            try {
+                ModernHttpRequester.processResponse(
                 httpResponseProcessor,
                 mResponse.code(),
                 new ResponseStreamAccessor() {
                   ...
-                });
+                });
+            } finally {
+                closeQuietly(responseDataStream);
+                closeQuietly(errorResponseDataStream);
+                if (mResponse != null) {
+                    ResponseBody body = mResponse.body();
+                    if (body != null) body.close();
+                    ResponseBody errorBody = mResponse.errorBody();
+                    if (errorBody != null) errorBody.close();
+                }
+            }
@@
+    private static void closeQuietly(@Nullable InputStream in) {
+        if (in != null) {
+            try { in.close(); } catch (IOException ignored) {}
+        }
+    }

Based on learnings.


73-75: Do error-body parsing off the UI thread

deliverResult runs on the main thread. Ensure error stream parsing happens in doTaskBackground (or is pre-buffered) to avoid UI jank. Optionally pre-buffer to bytes/string there and pass a ByteArrayInputStream (with a reasonable cap).

Example (illustrative):

@@
-                errorResponseDataStream = requester.getErrorResponseStream(mResponse);
+                ResponseBody eb = mResponse.errorBody();
+                if (eb != null) {
+                    byte[] bytes = eb.bytes(); // worker thread
+                    errorResponseDataStream = new java.io.ByteArrayInputStream(bytes);
+                } else {
+                    errorResponseDataStream = null;
+                }
app/src/org/commcare/activities/connect/ConnectActivity.java (1)

131-133: Typo in local variable name.

Rename notiificationsMenuItem → notificationsMenuItem for clarity.

Apply this diff:

-        MenuItem notiificationsMenuItem = menu.findItem(R.id.action_bell);
-        notiificationsMenuItem.setVisible(PersonalIdFeatureFlagChecker.isFeatureEnabled(NOTIFICATIONS));
+        MenuItem notificationsMenuItem = menu.findItem(R.id.action_bell);
+        notificationsMenuItem.setVisible(PersonalIdFeatureFlagChecker.isFeatureEnabled(NOTIFICATIONS));
app/src/org/commcare/connect/network/ConnectNetworkHelper.java (2)

144-146: Typo in log tag.

“Netowrk” → “Network”.

Apply this diff:

-                    Logger.log("Netowrk Error", error);
+                    Logger.log("Network Error", error);

374-381: Unify IO error logging with NetworkUtils.

Leverage centralized logging for consistency.

Apply this diff:

-                } else {
-                    Logger.exception("IO Exception during API call", exception);
-                    handler.processFailure(-1, url, "");
-                }
+                } else {
+                    NetworkUtils.logNetworkError(exception, url);
+                    handler.processFailure(-1, url, "");
+                }
app/src/org/commcare/connect/network/NetworkUtils.kt (1)

66-73: Nit: endpoint is non-null.

Elvis on a non-null parameter is redundant. Optional clean-up if you touch this next.

Also applies to: 85-91

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

61-65: processFailure: guard for non-JSON bodies and nulls; keep logs quiet

getErrorCodes() logs exceptions when bodies aren’t JSON. That’s fine, but consider short-circuiting when errorBody is empty to avoid noisy logs on HTML/text errors; pass a non-null string to Kotlin BaseApiCallback to avoid boundary NPEs.

-                Pair<String, String> errorCodes = getErrorCodes(errorBody);
+                Pair<String, String> errorCodes = getErrorCodes(errorBody == null ? "" : errorBody);
                 if (!handleErrorCodeIfPresent(errorCodes.getFirst(), errorCodes.getSecond(), sessionData)) {
                     super.processFailure(responseCode, url, errorBody);
                 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2689b7c and 338ddf4.

📒 Files selected for processing (34)
  • app/AndroidManifest.xml (1 hunks)
  • app/res/menu/menu_connect.xml (0 hunks)
  • app/res/values-es/strings.xml (1 hunks)
  • app/res/values-fr/strings.xml (1 hunks)
  • app/res/values-hi/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/strings.xml (1 hunks)
  • app/src/org/commcare/activities/InstallFromListActivity.java (1 hunks)
  • app/src/org/commcare/activities/PostRequestActivity.java (1 hunks)
  • app/src/org/commcare/activities/QueryRequestActivity.java (1 hunks)
  • app/src/org/commcare/activities/connect/ConnectActivity.java (5 hunks)
  • app/src/org/commcare/adapters/EntityStringFilterer.java (1 hunks)
  • app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java (1 hunks)
  • app/src/org/commcare/connect/MessageManager.java (5 hunks)
  • app/src/org/commcare/connect/database/JobStoreManager.java (1 hunks)
  • app/src/org/commcare/connect/network/ConnectNetworkHelper.java (7 hunks)
  • app/src/org/commcare/connect/network/IApiCallback.java (1 hunks)
  • app/src/org/commcare/connect/network/NetworkUtils.kt (1 hunks)
  • app/src/org/commcare/connect/network/base/BaseApi.kt (3 hunks)
  • app/src/org/commcare/connect/network/base/BaseApiCallback.kt (2 hunks)
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java (3 hunks)
  • app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java (1 hunks)
  • app/src/org/commcare/fragments/personalId/PersonalIdBiometricConfigFragment.java (1 hunks)
  • app/src/org/commcare/navdrawer/BaseDrawerController.kt (3 hunks)
  • app/src/org/commcare/network/GetAndParseActor.java (1 hunks)
  • app/src/org/commcare/personalId/PersonalIdFeatureFlagChecker.kt (2 hunks)
  • app/src/org/commcare/services/FCMMessageData.java (1 hunks)
  • app/src/org/commcare/tasks/DataPullTask.java (2 hunks)
  • app/src/org/commcare/tasks/EntityLoaderTask.java (1 hunks)
  • app/src/org/commcare/tasks/FormLoaderTask.java (1 hunks)
  • app/src/org/commcare/tasks/ModernHttpTask.java (3 hunks)
  • app/src/org/commcare/views/QuestionsView.java (2 hunks)
💤 Files with no reviewable changes (1)
  • app/res/menu/menu_connect.xml
🧰 Additional context used
🧠 Learnings (3)
📓 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.
📚 Learning: 2025-06-13T15:53:12.951Z
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.

Applied to files:

  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
📚 Learning: 2025-05-08T11:08:18.530Z
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.

Applied to files:

  • app/src/org/commcare/activities/connect/ConnectActivity.java
🧬 Code graph analysis (9)
app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java (2)
app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java (1)
  • FirebaseAnalyticsUtil (43-608)
app/src/org/commcare/google/services/analytics/AnalyticsParamValue.java (1)
  • AnalyticsParamValue (7-194)
app/src/org/commcare/navdrawer/BaseDrawerController.kt (1)
app/src/org/commcare/personalId/PersonalIdFeatureFlagChecker.kt (1)
  • isFeatureEnabled (21-28)
app/src/org/commcare/connect/MessageManager.java (1)
app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)
  • processFailure (20-53)
app/src/org/commcare/connect/network/base/BaseApi.kt (1)
app/src/org/commcare/connect/network/NetworkUtils.kt (1)
  • logFailedResponse (49-73)
app/src/org/commcare/connect/network/IApiCallback.java (1)
app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)
  • processFailure (20-53)
app/src/org/commcare/fragments/personalId/PersonalIdBiometricConfigFragment.java (2)
app/src/org/commcare/utils/BiometricsHelper.java (1)
  • BiometricsHelper (33-258)
app/src/org/commcare/android/database/connect/models/PersonalIdSessionData.kt (1)
  • PIN (33-35)
app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java (3)
app/src/org/commcare/connect/network/NetworkUtils.kt (1)
  • getErrorCodes (35-47)
app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)
  • processFailure (20-53)
app/src/org/commcare/connect/network/base/BaseApiHandler.kt (1)
  • onFailure (16-16)
app/src/org/commcare/activities/connect/ConnectActivity.java (3)
app/src/org/commcare/connect/ConnectConstants.java (1)
  • ConnectConstants (8-49)
app/src/org/commcare/connect/database/ConnectJobUtils.java (1)
  • ConnectJobUtils (25-383)
app/src/org/commcare/personalId/PersonalIdFeatureFlagChecker.kt (1)
  • isFeatureEnabled (21-28)
app/src/org/commcare/connect/network/ConnectNetworkHelper.java (2)
app/src/org/commcare/connect/network/NetworkUtils.kt (2)
  • getErrorBody (15-26)
  • logFailedResponse (49-73)
app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)
  • processFailure (20-53)
🔇 Additional comments (34)
app/src/org/commcare/fragments/personalId/PersonalIdBackupCodeFragment.java (1)

259-266: LGTM! Good addition of telemetry for locked account tracking.

The analytics call is appropriately placed to capture account lockout failures during the backup code recovery flow. The placement after logging the recovery result but before clearing fields and navigating ensures the event is captured at the right moment in the flow.

app/src/org/commcare/tasks/FormLoaderTask.java (1)

157-159: Good improvement to error visibility.

Logging exceptions during trace cleanup provides valuable debugging information that was previously lost.

app/src/org/commcare/tasks/DataPullTask.java (1)

285-287: Good improvement to error visibility.

Logging exceptions during trace cleanup provides valuable debugging information for both success and error paths.

Also applies to: 335-337

app/src/org/commcare/tasks/EntityLoaderTask.java (1)

72-74: Good improvement to error visibility.

Logging exceptions during trace cleanup provides valuable debugging information that was previously lost. This change is consistent with similar improvements across other task classes.

app/src/org/commcare/views/QuestionsView.java (2)

43-44: LGTM!

The AndroidX NonNull import is appropriate for the annotation used on the new helper method.


351-360: LGTM! Good defensive logging.

The catch-log-rethrow pattern appropriately captures diagnostic information about the IllegalArgumentException while preserving the original exception behavior. This will help investigate crashes related to view resizing.

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

327-329: No action required — errorStream non-usage is consistent with the codebase pattern.

The errorStream parameter is unused in InstallFromListActivity.processClientError(), but this is intentional and consistent with similar implementations (QueryRequestActivity, PostRequestActivity, GetAndParseActor). While ConnectNetworkHelper does parse the error body via NetworkUtils.getErrorBody(errorStream), it has a specialized use case requiring detailed error propagation to an IApiCallback handler.

InstallFromListActivity.handleRequestError() only accepts responseCode and a boolean flag, reflecting simpler error handling (retry or show results). To use the error body here would require modifying the method signature, which is outside the scope of this interface compliance update.

This design is appropriate for the activity's error-handling strategy.

app/AndroidManifest.xml (1)

5-5: Version name alignment looks good

Matching the manifest versionName to 2.60.0 is consistent with this back-merge of the 2.60 branch. No further changes needed.

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

346-348: New unlock variants look solid

Adding explicit PIN and fingerprint variants alongside the generic unlock message keeps the copy clear and matches the new flows.

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

274-276: French strings are consistent

The new PIN and fingerprint messages follow the existing tone and terminology—looks good.

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

276-278: Swahili copy reads well

The additional strings mirror the English intent while keeping terminology consistent with the rest of the locale.

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

267-269: Tigrinya updates align with the new flow

All three messages were added with consistent terminology—looks good from my side.

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

279-281: Portuguese variants are spot on

The new PIN and fingerprint instructions are clear and match the rest of the locale.

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

274-276: Localization strings properly formatted and consistent across all locales.

The Hindi localization strings are correctly updated with two new authentication verification messages (PIN and fingerprint) and capitalization standardization from "PersonalId" to "PersonalID". XML formatting is valid, and verification confirms all corresponding strings exist consistently across all locale files (English, Spanish, French, Portuguese, Swahili, and Tigrinya).

app/src/org/commcare/services/FCMMessageData.java (1)

123-129: Serialization logic is now correct, but note the breaking change.

The fix properly serializes both actionType (as enum string) and action (as raw string), which aligns with the class design where both fields serve different purposes:

  • actionType: parsed enum for programmatic use
  • action: original raw string from payload

The serialization order correctly matches the deserialization order in readExternal.

However, this format change is incompatible with previously serialized data (see comment on readExternal).

app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java (1)

566-576: Status filter removal verified — confirm this change is intentional.

The STATUS_APPROVED filter has been confirmed removed from the numberOfDeliveriesToday() method. In commit 2689b7c, it originally contained record.getStatus().equals(STATUS_APPROVED), but the current code no longer includes this condition.

This changes the behavior at line 626: all deliveries from today (approved, pending, rejected) now count toward the daily visit limit, whereas previously only approved deliveries counted. This could prevent users from submitting new deliveries if they have pending or rejected deliveries from today.

Confirm whether this change in counting logic is intentional or if the status filter should be retained.

app/src/org/commcare/tasks/ModernHttpTask.java (1)

100-105: Confirm getResponseStream() nullability annotation and verify external interface contract

The anonymous ResponseStreamAccessor implementation at line 94 is the only implementer in this codebase and properly implements getErrorResponseStream() with @Nullable annotation. However, there's an asymmetry: getErrorResponseStream() is annotated @Nullable but getResponseStream() is not, despite both fields being initialized to null and conditionally populated. If error responses (non-2xx, 204, HEAD) cause getResponseStream() to be called, it could return null and should also be annotated @Nullable for consistency.

Since ResponseStreamAccessor is from the external org.commcare.core.interfaces library, verify:

  • The interface contract for both methods' nullability
  • Whether other implementers exist outside this codebase
  • Whether getResponseStream() requires the @Nullable annotation to match its actual behavior

The javax.annotation.Nullable import is consistent with the file's convention.

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

176-178: Signature updated to match interface contract.

The processClientError method signature now accepts an InputStream errorStream parameter to align with the broader error-handling refactor across the codebase. Note that errorStream is currently unused in this implementation.

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

12-12: Interface signature updated to use String errorBody.

The processFailure method signature has been updated from (int, @Nullable InputStream, String) to (int, String, String), replacing the InputStream-based error handling with a String-based approach. This aligns with the broader network error-handling refactor across the codebase.

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

182-196: Signature updated to match interface contract.

The processClientError method signature now includes an InputStream errorStream parameter to align with the HttpResponseProcessor interface update. The parameter is currently unused as the implementation relies solely on the response code for error messaging.

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

83-85: Signature updated to match interface contract.

The processClientError override now accepts an InputStream errorStream parameter to comply with the updated HttpResponseProcessor interface. The parameter is unused as the implementation delegates to processErrorResponse which only requires the response code.

app/src/org/commcare/personalId/PersonalIdFeatureFlagChecker.kt (2)

15-15: New NOTIFICATIONS feature flag added.

The NOTIFICATIONS feature flag constant has been added to support notification-related feature toggling across the application. The implementation follows the established pattern for feature flags.


22-28: Feature flag handling implemented correctly.

The isFeatureEnabled method has been updated to handle the new NOTIFICATIONS flag, returning false to keep the feature disabled. The implementation maintains the exhaustive switch pattern with proper error handling for unknown flags.

app/src/org/commcare/connect/network/base/BaseApiCallback.kt (1)

20-53: Signature updated to match IApiCallback interface.

The processFailure method signature has been updated to accept errorBody as a String instead of an InputStream, aligning with the IApiCallback interface change. The error handling logic based on HTTP status codes remains unchanged, with the errorBody parameter available for future use.

app/src/org/commcare/navdrawer/BaseDrawerController.kt (4)

23-25: Feature flag imports added for cleaner code.

The companion functions isFeatureEnabled and feature flag constants are now imported directly, making the code more concise compared to qualifying calls with the class name.


242-245: Refactored to use imported feature flag helper.

The method now uses the imported isFeatureEnabled function for cleaner, more concise code.


247-249: Helper method consolidates notification visibility logic.

The new shouldShowNotiifcations method centralizes the logic for determining notification feature visibility based on login state and feature flag. Note: the typo in the method name is addressed in a previous comment.


131-145: Notification icon logic refactored to dedicated method.

The notification icon update logic has been extracted to updateNotificationsIcon(), making refreshDrawerContent cleaner and more maintainable.

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

90-92: All processFailure implementations updated consistently.

All five IApiCallback anonymous class implementations have been updated to match the new interface signature (int, String, String). Each implementation maintains its existing behavior of delegating to listener.connectActivityComplete(false). The url and errorBody parameters are available for future error logging or detailed error handling.

Also applies to: 142-144, 194-198, 254-256, 308-310

app/src/org/commcare/connect/network/base/BaseApi.kt (2)

48-56: Good: error stream parsed and deterministically closed.

getErrorBody + finally-close is correct and prevents resource leaks. No change needed.


60-65: Network failure path is consistent.

Logging + processNetworkFailure() is appropriate. LGTM.

app/src/org/commcare/fragments/personalId/PersonalIdBiometricConfigFragment.java (2)

134-153: Consolidated decision tree reads well.

Clear prioritization: fingerprint configured → agree; else hardware present → configure; PIN branch overlays correctly. LGTM.


161-174: Title/message normalization is sound.

Good fallback to no‑hardware error; avoids partial UI states. LGTM.

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

86-92: Defensive parse: avoid NumberFormatException on OPPORTUNITY_ID string.

Parsing untrusted extras can crash the activity. Add a safe parse with logging.

Apply this diff:

-        if (opportunityId == -1) {
-            String opportunityIdStr = getIntent().getStringExtra(ConnectConstants.OPPORTUNITY_ID);
-            if (!StringUtils.isEmpty(opportunityIdStr)) {
-                opportunityId = Integer.parseInt(opportunityIdStr);
-            }
-        }
+        if (opportunityId == -1) {
+            String opportunityIdStr = getIntent().getStringExtra(ConnectConstants.OPPORTUNITY_ID);
+            if (!StringUtils.isEmpty(opportunityIdStr)) {
+                try {
+                    opportunityId = Integer.parseInt(opportunityIdStr.trim());
+                } catch (NumberFormatException nfe) {
+                    org.javarosa.core.services.Logger.exception(
+                            "Invalid OPPORTUNITY_ID extra (non-integer): " + opportunityIdStr, nfe);
+                }
+            }
+        }
⛔ Skipped due to learnings
Learnt from: OrangeAndGreen
PR: dimagi/commcare-android#3248
File: app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java:0-0
Timestamp: 2025-07-29T14:10:55.131Z
Learning: In ConnectUnlockFragment.java, opportunityId values are expected to always contain valid integer strings. Integer.parseInt() should be allowed to throw NumberFormatException if invalid data is encountered, following the fail-fast philosophy used throughout the Connect feature for data contract violations.

Comment on lines +212 to +214
<string name="connect_verify_message">Configura un método a continuación para desbloquear tanto tu dispositivo como tu cuenta de PersonalID</string>
<string name="connect_verify_message_pin">Configure un PIN o patrón a continuación para desbloquear su dispositivo y su cuenta PersonalID</string>
<string name="connect_verify_message_fingerprint">Configure su huella digital a continuación para desbloquear tanto su dispositivo como su cuenta PersonalID</string>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep the imperative tone consistent

Line 212 retains the informal “Configura…”, but the newly added lines switch to the formal “Configure…”. Please align the new strings with the existing tone.

-    <string name="connect_verify_message_pin">Configure un PIN o patrón a continuación para desbloquear su dispositivo y su cuenta PersonalID</string>
-    <string name="connect_verify_message_fingerprint">Configure su huella digital a continuación para desbloquear tanto su dispositivo como su cuenta PersonalID</string>
+    <string name="connect_verify_message_pin">Configura un PIN o patrón a continuación para desbloquear tu dispositivo y tu cuenta PersonalID</string>
+    <string name="connect_verify_message_fingerprint">Configura tu huella digital a continuación para desbloquear tanto tu dispositivo como tu cuenta PersonalID</string>
📝 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
<string name="connect_verify_message">Configura un método a continuación para desbloquear tanto tu dispositivo como tu cuenta de PersonalID</string>
<string name="connect_verify_message_pin">Configure un PIN o patrón a continuación para desbloquear su dispositivo y su cuenta PersonalID</string>
<string name="connect_verify_message_fingerprint">Configure su huella digital a continuación para desbloquear tanto su dispositivo como su cuenta PersonalID</string>
<string name="connect_verify_message">Configura un método a continuación para desbloquear tanto tu dispositivo como tu cuenta de PersonalID</string>
<string name="connect_verify_message_pin">Configura un PIN o patrón a continuación para desbloquear tu dispositivo y tu cuenta PersonalID</string>
<string name="connect_verify_message_fingerprint">Configura tu huella digital a continuación para desbloquear tanto tu dispositivo como tu cuenta PersonalID</string>
🤖 Prompt for AI Agents
In app/res/values-es/strings.xml around lines 212 to 214, the existing string at
line 212 uses the informal imperative "Configura..." while the two new strings
on 213–214 use the formal "Configure..."; update the new strings to the informal
imperative to match tone—replace "Configure" with "Configura" in both
connect_verify_message_pin and connect_verify_message_fingerprint so all three
strings use the same informal phrasing.

Comment on lines +70 to 85
private boolean handleErrorCodeIfPresent(String errorCode, String errorSubCode, PersonalIdSessionData sessionData) {
try {
if (errorResponse != null) {
byte[] errorBytes = StreamsUtil.inputStreamToByteArray(errorResponse);
String jsonStr = new String(errorBytes, java.nio.charset.StandardCharsets.UTF_8);
JSONObject json = new JSONObject(jsonStr);

String errorCode = json.optString("error_code", "");
sessionData.setSessionFailureCode(errorCode);
if ("LOCKED_ACCOUNT".equalsIgnoreCase(errorCode)) {
onFailure(PersonalIdOrConnectApiErrorCodes.ACCOUNT_LOCKED_ERROR, null);
return true;
} else if ("INTEGRITY_ERROR".equalsIgnoreCase(errorCode)) {
if (json.has("sub_code")) {
String subErrorCode = json.optString("sub_code");
Logger.log(LogTypes.TYPE_MAINTENANCE, "Integrity error with subcode " + subErrorCode);
sessionData.setSessionFailureSubcode(subErrorCode);
onFailure(PersonalIdOrConnectApiErrorCodes.INTEGRITY_ERROR, null);
}
return true;
}
sessionData.setSessionFailureCode(errorCode);
if ("LOCKED_ACCOUNT".equalsIgnoreCase(errorCode)) {
onFailure(PersonalIdOrConnectApiErrorCodes.ACCOUNT_LOCKED_ERROR, null);
return true;
} else if ("INTEGRITY_ERROR".equalsIgnoreCase(errorCode)) {
Logger.log(LogTypes.TYPE_MAINTENANCE, "Integrity error with subcode " + errorSubCode);
sessionData.setSessionFailureSubcode(errorSubCode);
onFailure(PersonalIdOrConnectApiErrorCodes.INTEGRITY_ERROR, null);
return true;
}
} catch (Exception e) {
Logger.exception("Error parsing error_code", e);
Logger.exception("Error handling error code", e);
}
return false;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t clobber session failure code/subcode when empty

When parsing fails or codes aren’t present, you set empty strings on sessionData, wiping prior state.

-            sessionData.setSessionFailureCode(errorCode);
+            if (errorCode != null && !errorCode.isEmpty()) {
+                sessionData.setSessionFailureCode(errorCode);
+            }
@@
-                sessionData.setSessionFailureSubcode(errorSubCode);
+                if (errorSubCode != null && !errorSubCode.isEmpty()) {
+                    sessionData.setSessionFailureSubcode(errorSubCode);
+                }
📝 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 boolean handleErrorCodeIfPresent(String errorCode, String errorSubCode, PersonalIdSessionData sessionData) {
try {
if (errorResponse != null) {
byte[] errorBytes = StreamsUtil.inputStreamToByteArray(errorResponse);
String jsonStr = new String(errorBytes, java.nio.charset.StandardCharsets.UTF_8);
JSONObject json = new JSONObject(jsonStr);
String errorCode = json.optString("error_code", "");
sessionData.setSessionFailureCode(errorCode);
if ("LOCKED_ACCOUNT".equalsIgnoreCase(errorCode)) {
onFailure(PersonalIdOrConnectApiErrorCodes.ACCOUNT_LOCKED_ERROR, null);
return true;
} else if ("INTEGRITY_ERROR".equalsIgnoreCase(errorCode)) {
if (json.has("sub_code")) {
String subErrorCode = json.optString("sub_code");
Logger.log(LogTypes.TYPE_MAINTENANCE, "Integrity error with subcode " + subErrorCode);
sessionData.setSessionFailureSubcode(subErrorCode);
onFailure(PersonalIdOrConnectApiErrorCodes.INTEGRITY_ERROR, null);
}
return true;
}
sessionData.setSessionFailureCode(errorCode);
if ("LOCKED_ACCOUNT".equalsIgnoreCase(errorCode)) {
onFailure(PersonalIdOrConnectApiErrorCodes.ACCOUNT_LOCKED_ERROR, null);
return true;
} else if ("INTEGRITY_ERROR".equalsIgnoreCase(errorCode)) {
Logger.log(LogTypes.TYPE_MAINTENANCE, "Integrity error with subcode " + errorSubCode);
sessionData.setSessionFailureSubcode(errorSubCode);
onFailure(PersonalIdOrConnectApiErrorCodes.INTEGRITY_ERROR, null);
return true;
}
} catch (Exception e) {
Logger.exception("Error parsing error_code", e);
Logger.exception("Error handling error code", e);
}
return false;
private boolean handleErrorCodeIfPresent(String errorCode, String errorSubCode, PersonalIdSessionData sessionData) {
try {
if (errorCode != null && !errorCode.isEmpty()) {
sessionData.setSessionFailureCode(errorCode);
}
if ("LOCKED_ACCOUNT".equalsIgnoreCase(errorCode)) {
onFailure(PersonalIdOrConnectApiErrorCodes.ACCOUNT_LOCKED_ERROR, null);
return true;
} else if ("INTEGRITY_ERROR".equalsIgnoreCase(errorCode)) {
Logger.log(LogTypes.TYPE_MAINTENANCE, "Integrity error with subcode " + errorSubCode);
if (errorSubCode != null && !errorSubCode.isEmpty()) {
sessionData.setSessionFailureSubcode(errorSubCode);
}
onFailure(PersonalIdOrConnectApiErrorCodes.INTEGRITY_ERROR, null);
return true;
}
} catch (Exception e) {
Logger.exception("Error handling error code", e);
}
return false;
}
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
around lines 70 to 85, avoid overwriting existing session failure code/subcode
with empty or null values: only call
sessionData.setSessionFailureCode(errorCode) and
sessionData.setSessionFailureSubcode(errorSubCode) when the provided values are
non-null and non-empty (use StringUtils.isEmpty or equivalent checks),
preserving prior values when parsing fails or codes are absent; keep the
existing error-specific handling (LOCKED_ACCOUNT/INTEGRITY_ERROR) and logging
intact, and ensure you do not set empty strings before invoking onFailure.

Comment on lines +16 to +26
fun getErrorBody(stream: InputStream?): String {
try {
if (stream != null) {
val errorBytes = StreamsUtil.inputStreamToByteArray(stream)
return String(errorBytes, StandardCharsets.UTF_8)
}
} catch (e: Exception) {
Logger.exception("Error parsing error_code", e);
}
return ""
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Close the stream inside getErrorBody to make callers safer.

Using use ensures closure even on exceptions and reduces duplication/leak risk.

Apply this diff:

-    fun getErrorBody(stream: InputStream?): String {
-        try {
-            if (stream != null) {
-                val errorBytes = StreamsUtil.inputStreamToByteArray(stream)
-                return String(errorBytes, StandardCharsets.UTF_8)
-            }
-        } catch (e: Exception) {
-            Logger.exception("Error parsing error_code", e);
-        }
-        return ""
-    }
+    fun getErrorBody(stream: InputStream?): String {
+        if (stream == null) return ""
+        return try {
+            stream.use { input ->
+                val errorBytes = StreamsUtil.inputStreamToByteArray(input)
+                String(errorBytes, StandardCharsets.UTF_8)
+            }
+        } catch (e: Exception) {
+            Logger.exception("Error parsing error_code", 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
fun getErrorBody(stream: InputStream?): String {
try {
if (stream != null) {
val errorBytes = StreamsUtil.inputStreamToByteArray(stream)
return String(errorBytes, StandardCharsets.UTF_8)
}
} catch (e: Exception) {
Logger.exception("Error parsing error_code", e);
}
return ""
}
fun getErrorBody(stream: InputStream?): String {
if (stream == null) return ""
return try {
stream.use { input ->
val errorBytes = StreamsUtil.inputStreamToByteArray(input)
String(errorBytes, StandardCharsets.UTF_8)
}
} catch (e: Exception) {
Logger.exception("Error parsing error_code", e)
""
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/connect/network/NetworkUtils.kt around lines 16 to 26,
the InputStream passed into getErrorBody is not closed on all paths; change the
method to call stream?.use { ... } so the stream is automatically closed even if
StreamsUtil.inputStreamToByteArray or String conversion throws; inside the use
block read the bytes and return the UTF-8 string, keep the catch to log
exceptions, and return an empty string on null/exception — this removes manual
close duplication and prevents leaks.

Comment on lines 223 to 233
private fun updateNotificationsIcon() {
if(shouldShowNotiifcations()) {
val notifications = NotificationRecordDatabaseHelper().getAllNotifications(activity)
val hasUnreadNotification = notifications!!.any { !it.readStatus }

binding.ivNotification.setImageResource(
if (hasUnreadNotification) R.drawable.ic_new_notification_bell
else R.drawable.ic_bell
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in method name.

The method name contains a typo: shouldShowNotiifcations should be shouldShowNotifications (extra 'i'). This typo appears in multiple locations (lines 224, 239, 247).

Apply this diff to fix the typo:

-    private fun updateNotificationsIcon() {
-        if(shouldShowNotiifcations()) {
+    private fun updateNotificationsIcon() {
+        if(shouldShowNotifications()) {
             val notifications = NotificationRecordDatabaseHelper().getAllNotifications(activity)
             val hasUnreadNotification = notifications!!.any { !it.readStatus }
 
             binding.ivNotification.setImageResource(
                 if (hasUnreadNotification) R.drawable.ic_new_notification_bell
                 else R.drawable.ic_bell
             )
         }
     }

And update the other occurrences:

-        binding.notificationView.visibility = if (shouldShowNotiifcations()) View.VISIBLE else View.GONE
+        binding.notificationView.visibility = if (shouldShowNotifications()) View.VISIBLE else View.GONE
-    private fun shouldShowNotiifcations(): Boolean {
+    private fun shouldShowNotifications(): Boolean {
         return PersonalIdManager.getInstance().isloggedIn() && isFeatureEnabled(NOTIFICATIONS);
     }
📝 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 fun updateNotificationsIcon() {
if(shouldShowNotiifcations()) {
val notifications = NotificationRecordDatabaseHelper().getAllNotifications(activity)
val hasUnreadNotification = notifications!!.any { !it.readStatus }
binding.ivNotification.setImageResource(
if (hasUnreadNotification) R.drawable.ic_new_notification_bell
else R.drawable.ic_bell
)
}
}
private fun updateNotificationsIcon() {
if(shouldShowNotifications()) {
val notifications = NotificationRecordDatabaseHelper().getAllNotifications(activity)
val hasUnreadNotification = notifications!!.any { !it.readStatus }
binding.ivNotification.setImageResource(
if (hasUnreadNotification) R.drawable.ic_new_notification_bell
else R.drawable.ic_bell
)
}
}
🤖 Prompt for AI Agents
In app/src/org/commcare/navdrawer/BaseDrawerController.kt around lines 223 to
233 (and other occurrences at lines 224, 239, 247), the method name is
misspelled as shouldShowNotiifcations; rename all occurrences (calls and the
method declaration) to shouldShowNotifications, update import/overrides/usages
accordingly, and ensure compilation by refactoring any references across the
file to the corrected name.

Comment on lines 114 to 120
public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
actionType = ActionTypes.valueOf(ExtUtil.readString(in));
action = ExtUtil.readString(in);
username = ExtUtil.readString(in);
domain = ExtUtil.readString(in);
creationTime = new DateTime(ExtUtil.readLong(in));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical: Breaking change in serialization format requires migration strategy.

Adding the action field read at line 116 changes the serialization format and breaks backward compatibility. Any existing serialized FCMMessageData objects will fail to deserialize because:

  1. Old format wrote only one string (action) at the position where actionType is now expected
  2. The new code expects two strings in sequence (actionType, then action)
  3. Reading old data will cause field misalignment or deserialization failure

Additionally, line 115 uses ActionTypes.valueOf() which throws IllegalArgumentException if the string doesn't match a valid enum constant, with no error handling.

Verify the following:

Consider adding:

  1. A migration strategy or version field to handle old serialized data
  2. Error handling around ActionTypes.valueOf():
 @Override
 public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
-    actionType = ActionTypes.valueOf(ExtUtil.readString(in));
+    try {
+        actionType = ActionTypes.valueOf(ExtUtil.readString(in));
+    } catch (IllegalArgumentException e) {
+        Logger.log("FCMMessageData", "Invalid action type during deserialization, defaulting to INVALID");
+        actionType = ActionTypes.INVALID;
+    }
     action = ExtUtil.readString(in);
     username = ExtUtil.readString(in);
     domain = ExtUtil.readString(in);
     creationTime = new DateTime(ExtUtil.readLong(in));
 }

🏁 Script executed:

#!/bin/bash
# Search for any persisted FCMMessageData instances that need migration
rg -n "FCMMessageData" --type java -C3 | rg -i "serializ|persist|save|store"

Length of output: 2097


The web search results confirm the critical nature of the issue. Externalizable classes are responsible for their own versioning which is tied to the external format, and all data and objects must be read from or skipped in the stream in the same order as they were written. Adding a field read at line 116 violates this contract without versioning support.

Now let me check the actual writeExternal method and ActionTypes enum:

Based on CommCare's own documentation, when code is changed for a serializable class, a data migration check is needed, especially when adding a new field to the class that you want to persist across serializations. Let me verify the writeExternal method in the repository:


🏁 Script executed:

#!/bin/bash
# Find writeExternal method in FCMMessageData
rg -n "writeExternal" "app/src/org/commcare/services/FCMMessageData.java" -A10

Length of output: 452


🏁 Script executed:

#!/bin/bash
# Find ActionTypes enum definition
rg -n "enum ActionTypes" --type java -A20

Length of output: 1939


🏁 Script executed:

#!/bin/bash
# Check if there's any versioning mechanism in the class
rg -n "serialVersionUID|version|VERSION" "app/src/org/commcare/services/FCMMessageData.java"

Length of output: 0


Critical: Breaking change in serialization format requires migration strategy for existing persisted data.

The code adds actionType as a new first field in the external format (line 124 changed from writing action to writing actionType.toString()), breaking backward compatibility with all existing serialized FCMMessageData objects stored in SharedPreferences and Bundles.

When old data is deserialized by the new code:

  • Line 115 reads the old action string as actionType (invalid enum value)
  • Line 116 reads the old username as action
  • Field alignment fails, causing deserialization crashes on app upgrade

Additionally, ActionTypes.valueOf() at line 115 throws IllegalArgumentException with no error handling. While ActionTypes.INVALID exists as a fallback, add try-catch to handle invalid enum values gracefully:

 @Override
 public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
-    actionType = ActionTypes.valueOf(ExtUtil.readString(in));
+    try {
+        actionType = ActionTypes.valueOf(ExtUtil.readString(in));
+    } catch (IllegalArgumentException e) {
+        actionType = ActionTypes.INVALID;
+    }
     action = ExtUtil.readString(in);
     username = ExtUtil.readString(in);
     domain = ExtUtil.readString(in);
     creationTime = new DateTime(ExtUtil.readLong(in));
 }

Required: Implement a data migration strategy or versioning mechanism in readExternal() to handle existing serialized data from users upgrading the app.

📝 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 readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
actionType = ActionTypes.valueOf(ExtUtil.readString(in));
action = ExtUtil.readString(in);
username = ExtUtil.readString(in);
domain = ExtUtil.readString(in);
creationTime = new DateTime(ExtUtil.readLong(in));
}
public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
try {
actionType = ActionTypes.valueOf(ExtUtil.readString(in));
} catch (IllegalArgumentException e) {
actionType = ActionTypes.INVALID;
}
action = ExtUtil.readString(in);
username = ExtUtil.readString(in);
domain = ExtUtil.readString(in);
creationTime = new DateTime(ExtUtil.readLong(in));
}

@avazirna
Copy link
Contributor Author

@damagatchi retest this please

4 similar comments
@avazirna
Copy link
Contributor Author

@damagatchi retest this please

@avazirna
Copy link
Contributor Author

@damagatchi retest this please

@avazirna
Copy link
Contributor Author

@damagatchi retest this please

@avazirna
Copy link
Contributor Author

@damagatchi retest this please

@avazirna
Copy link
Contributor Author

@damagatchi retest this please

1 similar comment
@avazirna
Copy link
Contributor Author

@damagatchi retest this please

@avazirna avazirna merged commit e51a6ef into master Oct 24, 2025
6 of 10 checks passed
@avazirna avazirna deleted the commcare_2.60 branch October 24, 2025 10:06
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.

5 participants