Skip to content

Conversation

@Jignesh-dimagi
Copy link
Contributor

@Jignesh-dimagi Jignesh-dimagi commented Jan 6, 2026

Product Description

There are no visible changes in this PR

Technical Summary

https://dimagi.atlassian.net/browse/CCCT-1888

API refactored to convert for async using retrofit

Connect Messaging APIs:

  1. Send message API
  2. Update channel consent
  3. Retrieve encryption key for channel

OAuth token calls:

  1. Retrieving the Connect token API
  2. Retrieving the HQ token

Other API:

  1. Linking HQ and Connect

Safety Story

  1. Tested the following for connect messaging
  • Retrieving the messaging channels
  • Fetching the encryption keys for these channels
  • Sending messages
  • Receiving messages
  1. Tested the following for OAuth token calls
  • Retrieved personalId token
  • Retrieved HQ token
  1. Tested the following for Linking HQ and Connect
  • I tried linking the connect and HQ but server was giving 500 error, I have seen this previously also as my connect account is already linked to HQ. QA needs to verify it.
  1. Tested the following for form submission
  • Syncing forms for HQ

QA Plan

QA should test following:

  1. Connect messaging - sending, receiving and consent
  2. Linking connect user to HQ user
  3. Syncing forms inside the HQ app with a Personal ID login

Labels and Review

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

@Jignesh-dimagi Jignesh-dimagi added the skip-integration-tests Skip android tests. label Jan 6, 2026
@Jignesh-dimagi Jignesh-dimagi changed the title Jignesh/feat/ccct 1888 Refactor API calls Jan 6, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

This PR refactors the Connect networking layer to standardize asynchronous API handling through a PersonalIdApiHandler abstraction. It removes hardcoded URL string resources, adds new Retrofit-based API endpoints for channel consent and messaging operations, migrates AsyncTask-based token retrieval to CompletableFuture in ConnectSsoHelper, and introduces parser classes for processing HQ token retrieval, channel encryption key, and HQ worker linking responses. Public method signatures are largely preserved; internal implementation shifts to callback-based handler patterns with centralized error handling.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/MessageManager
    participant Handler as PersonalIdApiHandler
    participant ApiPersonalId
    participant Retrofit as Retrofit ApiService
    participant Backend as Backend API
    participant Callback as Response Callback/Parser

    rect rgb(220, 245, 220)
    note right of Client: Channel Consent Flow
    Client->>Handler: updateChannelConsent(context, user, channel)
    Handler->>ApiPersonalId: updateChannelConsent(context, user, channel)
    ApiPersonalId->>Retrofit: POST /messaging/update_consent/
    Retrofit->>Backend: Send request with token & headers
    Backend-->>Retrofit: 200 Response
    Retrofit-->>Callback: onResponse(Response)
    Callback->>Handler: onSuccess(Boolean)
    Handler->>Client: Notify listener
    end

    rect rgb(220, 245, 240)
    note right of Client: Channel Encryption Key Retrieval
    Client->>Handler: retrieveChannelEncryptionKey(context, user, channel)
    Handler->>ApiPersonalId: retrieveChannelEncryptionKey(context, user, channel)
    ApiPersonalId->>Retrofit: POST with channelUrl
    Retrofit->>Backend: Send request
    Backend-->>Retrofit: JSON with key
    Retrofit-->>Callback: onResponse(InputStream)
    Callback->>Callback: parse(JSON) → extract key
    Callback->>Callback: storeMessagingChannel(channel)
    Handler->>Client: onSuccess(Boolean)
    end

    rect rgb(245, 230, 220)
    note right of Client: Send Messaging Flow
    Client->>Handler: sendMessagingMessage(context, user, message, channel)
    Handler->>ApiPersonalId: sendMessagingMessage(context, user, message, channel)
    ApiPersonalId->>Retrofit: POST /messaging/send_message/
    Retrofit->>Backend: Send with encryption key
    Backend-->>Retrofit: 200 OK
    Retrofit-->>Callback: onResponse(Response)
    Callback->>Handler: onSuccess(Boolean)
    Handler->>Client: Mark message confirmed & persist
    end

    rect rgb(240, 230, 245)
    note right of Client: HQ Token Async Retrieval
    Client->>Handler: retrieveHqTokenASync(context, hqUsername, connectToken)
    Handler->>ApiPersonalId: retrieveHqTokenASync(context, hqUsername, connectToken)
    ApiPersonalId->>Retrofit: GET /token endpoint
    Retrofit->>Backend: Send request
    Backend-->>Retrofit: JSON {token, expires}
    Retrofit-->>Callback: onResponse(InputStream)
    Callback->>Callback: parse(JSON) → TokenAuth
    Callback->>Callback: storeHqToken(context, seatedAppId, hqUsername)
    Handler->>Client: onSuccess(TokenAuth)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

QA Note

Suggested reviewers

  • OrangeAndGreen
  • shubham1g5
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.85% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Refactor API calls' is vague and generic, lacking specificity about which APIs were refactored or the nature of the refactoring (converting to async, Retrofit integration, etc.). Consider revising the title to be more specific, such as 'Refactor messaging and OAuth APIs to use async Retrofit' or 'Convert Connect messaging and token APIs to async with Retrofit'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description covers key areas including product description, technical summary with ticket reference, safety story with testing details, and QA plan. All major template sections are addressed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 jignesh/feat/ccct-1888

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 (1)
app/src/org/commcare/connect/MessageManager.java (1)

83-106: Missing listener callback when channel is null or key is empty.

When channel is null or the key is empty (line 87), the method logs an error but never invokes listener.connectActivityComplete(false). This leaves the caller waiting indefinitely without notification of failure.

Proposed fix
         } else {
             Logger.log("Messaging", "Tried to send message but no encryption key");
+            listener.connectActivityComplete(false);
         }
🤖 Fix all issues with AI Agents
In @app/src/org/commcare/connect/MessageManager.java:
- Around line 54-69: Fix the typo in the log message inside
getChannelEncryptionKey: update the Logger.log call in the
PersonalIdApiHandler.onFailure override to use "retrieve" instead of "retreive"
so the message reads "Failed to retrieve encryption key: ..." (locate the
Logger.log in getChannelEncryptionKey / PersonalIdApiHandler.onFailure to change
the string).

In
@app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt:
- Around line 26-34: The parser currently treats an empty HTTP body as a
successful response and only catches JSONException; update retrieve logic in
RetrieveChannelEncryptionKeyResponseParser (the block that reads
responseAsString, parses JSONObject, calls channel.setKey(...) and
ConnectMessagingDatabaseHelper.storeMessagingChannel(...)) so that an empty
response is treated as an error (e.g., throw a RuntimeException or return a
failure rather than returning true) and ensure
StreamsUtil.inputStreamToByteArray() IOExceptions are caught and handled the
same way as the JSONException so missing/empty bodies or IO failures don’t
silently succeed.

In
@app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt:
- Line 41: CommCareApplication.instance().getCurrentApp() can be null, so before
calling getUniqueId() in RetrieveHqTokenResponseParser (where you create
seatedAppId and then call SsoToken and ConnectDatabaseHelper.storeHqToken),
fetch currentApp into a local variable, check it for null, and only call
currentApp.getUniqueId() and store the token when non-null; in the else branch
handle the no-app-seated case (e.g., log an error via the parser's logger and
return/skip storing) so you avoid an NPE.

In @app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java:
- Around line 348-362: Rename the misspelled parameter `messsage` to `message`
in the method signature of sendMessagingMessage and update all references inside
the method body (e.g., the argument passed to
ApiPersonalId.sendMessagingMessage) to use the corrected name; ensure the
parameter type remains ConnectMessagingMessageRecord and no other behavior
changes are introduced.

In @app/src/org/commcare/connect/network/ConnectSsoHelper.java:
- Around line 261-266: Fix the typo "workder" to "worker" in the failure logging
inside the onFailure callback in ConnectSsoHelper: update both Logger.exception
calls (the one logging the Throwable t and the one constructing a new Throwable)
so the message reads "Failed to link HQ worker" and keep existing Throwable
usage and context intact.
🧹 Nitpick comments (5)
app/src/org/commcare/connect/network/ApiEndPoints.java (1)

25-27: Minor formatting inconsistency.

Line 26 is missing a space after the = sign, unlike line 25 and other constants in this file.

🔎 Suggested fix
     public static final String CONNECT_MESSAGE_CHANNEL_CONSENT_URL = "/messaging/update_consent/";
-    public static final String CONNECT_MESSAGE_SEND_URL ="/messaging/send_message/";
+    public static final String CONNECT_MESSAGE_SEND_URL = "/messaging/send_message/";
app/src/org/commcare/connect/network/connectId/parser/LinkHqWorkerResponseParser.kt (1)

16-20: Consider null-safety check for anyInputObject.

The cast on line 17 will throw a ClassCastException if anyInputObject is null or not a ConnectLinkedAppRecord. Based on the fail-fast guidance for Kotlin, an explicit check with a clear error message would help debugging.

🔎 Suggested improvement
     ): T {
-        val appRecord: ConnectLinkedAppRecord = anyInputObject as ConnectLinkedAppRecord
+        val appRecord: ConnectLinkedAppRecord = anyInputObject as? ConnectLinkedAppRecord
+            ?: throw IllegalArgumentException("Expected ConnectLinkedAppRecord but got ${anyInputObject?.javaClass?.name}")
         appRecord.setWorkerLinked(true)
         ConnectAppDatabaseUtil.storeApp(context, appRecord)
         return true as T
     }
app/src/org/commcare/connect/network/ConnectSsoHelper.java (1)

161-169: InterruptedException handling discards thread interrupt status.

When catching InterruptedException, the thread's interrupt status is lost. Best practice is to restore it by calling Thread.currentThread().interrupt(). Also consider adding a timeout to completableFuture.get() to prevent indefinite blocking.

🔎 Suggested improvement
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 ...
             try {
-                return completableFuture.get();
+                return completableFuture.get(30, TimeUnit.SECONDS);
             } catch (InterruptedException | ExecutionException e) {
+                if (e instanceof InterruptedException) {
+                    Thread.currentThread().interrupt();
+                }
                 if(e.getCause() instanceof TokenDeniedException){
                     throw new TokenDeniedException();
                 }else{
                     throw new TokenUnavailableException();
                 }
+            } catch (TimeoutException e) {
+                throw new TokenUnavailableException();
             }
app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt (1)

25-32: Consider adding null-safety and IOException handling.

The cast on line 25 will throw if anyInputObject is null. Additionally, StreamsUtil.inputStreamToByteArray() can throw IOException which is not caught.

🔎 Suggested improvement
+import java.io.IOException
+
     override fun parse(
         responseCode: Int,
         responseData: InputStream,
         anyInputObject: Any?,
     ): T {
-        val hqUsername: String = anyInputObject as String
+        val hqUsername: String = anyInputObject as? String
+            ?: throw IllegalArgumentException("Expected String (hqUsername) but got ${anyInputObject?.javaClass?.name}")
         try {
             val responseAsString =
                 String(
                     StreamsUtil.inputStreamToByteArray(
                         responseData,
                     ),
                 )
             // ... rest of parsing
-        } catch (e: JSONException) {
+        } catch (e: Exception) {
+            when (e) {
+                is JSONException, is IOException -> throw RuntimeException(e)
+                else -> throw e
+            }
-            throw RuntimeException(e)
         }
     }
app/src/org/commcare/connect/network/ApiPersonalId.java (1)

167-189: Consider extracting HQ OAuth client ID to a constant.

The client ID "4eHlQad1oasGZF0lPiycZIjyL0SY1zx7ZblA6SCV" is duplicated here (line 169) and in retrieveHqTokenSync (line 194). Extracting it to a named constant would improve maintainability and prevent drift.

Proposed refactor
 public class ApiPersonalId {
     private static final String API_VERSION_NONE = null;
     private static final String CONNECT_CLIENT_ID = "zqFUtAAMrxmjnC1Ji74KAa6ZpY1mZly0J0PlalIa";
+    private static final String HQ_OAUTH_CLIENT_ID = "4eHlQad1oasGZF0lPiycZIjyL0SY1zx7ZblA6SCV";

Then use HQ_OAUTH_CLIENT_ID in both methods.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 721a656 and 76e1cf5.

📒 Files selected for processing (10)
  • app/res/values/strings.xml
  • app/src/org/commcare/connect/MessageManager.java
  • app/src/org/commcare/connect/network/ApiEndPoints.java
  • app/src/org/commcare/connect/network/ApiPersonalId.java
  • app/src/org/commcare/connect/network/ApiService.java
  • app/src/org/commcare/connect/network/ConnectSsoHelper.java
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/network/connectId/parser/LinkHqWorkerResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt
💤 Files with no reviewable changes (1)
  • app/res/values/strings.xml
🧰 Additional context used
🧠 Learnings (24)
📓 Common learnings
Learnt from: shubham1g5
Repo: dimagi/commcare-android PR: 0
File: :0-0
Timestamp: 2025-05-08T11:08:18.530Z
Learning: PR #3048 "Phase 4 Connect PR" introduces a substantial feature called "Connect" to the CommCare Android app, which includes messaging, job management, delivery tracking, payment processing, authentication flows, and learning modules. It follows a modern architecture using Navigation Components with three navigation graphs, segregated business logic in Manager classes, and proper database persistence.
Learnt from: Jignesh-dimagi
Repo: dimagi/commcare-android PR: 0
File: :0-0
Timestamp: 2025-05-08T13:40:19.645Z
Learning: PR #3048 introduces a comprehensive messaging system in the Connect feature, implementing secure encryption using AES-GCM for message content, proper channel management with consent flows, and a well-designed UI separation between sent and received messages with real-time notification integration.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/connect/MessageManager.java:0-0
Timestamp: 2025-07-29T14:54:47.940Z
Learning: User OrangeAndGreen organizes messaging-related changes into separate PRs rather than mixing them with other Connect work, which aligns with their preference for handling code issues in separate PRs when they are not directly related to the main changes.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/connect/network/ApiPersonalId.java:0-0
Timestamp: 2025-07-29T14:56:40.681Z
Learning: User OrangeAndGreen prefers to defer ApiPersonalId refactoring (specifically InputStream resource management improvements) to separate PRs rather than mixing them with Connect feature work, even when the code is already in master.
Learnt from: pm-dimagi
Repo: dimagi/commcare-android PR: 2847
File: app/src/org/commcare/connect/network/connectId/ApiService.java:8-63
Timestamp: 2025-01-28T09:38:59.882Z
Learning: The ConnectID API service methods should use Map<String, String> for request bodies and ResponseBody for responses, as per team preference.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 2912
File: app/src/org/commcare/fragments/connectId/ConnectIdPasswordVerificationFragment.java:206-213
Timestamp: 2025-01-21T18:20:43.883Z
Learning: In the Connect ID feature, server responses containing user data should be parsed using ConnectUserResponseParser to ensure consistent handling of payment information (owner_name and phone_number) and secondary phone verification across all authentication flows.
Learnt from: pm-dimagi
Repo: dimagi/commcare-android PR: 2949
File: app/src/org/commcare/fragments/connectId/ConnectIdPinFragment.java:244-275
Timestamp: 2025-02-04T21:22:56.537Z
Learning: Direct JSONObject parsing is acceptable for handling user data responses in ConnectIdPinFragment, as decided by the team. No need to enforce ConnectUserResponseParser usage in this context.
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/connect/MessageManager.java:0-0
Timestamp: 2025-07-29T14:45:13.470Z
Learning: In the CommCare Android Connect messaging system (MessageManager.java), the error handling logic in updateChannelConsent's processFailure callback intentionally attempts to retrieve the channel encryption key even after consent update failure. This behavior was recommended by the server team and the worst case scenario is silently failing to retrieve the encryption key, which is considered acceptable.
📚 Learning: 2025-05-08T13:40:19.645Z
Learnt from: Jignesh-dimagi
Repo: dimagi/commcare-android PR: 0
File: :0-0
Timestamp: 2025-05-08T13:40:19.645Z
Learning: PR #3048 introduces a comprehensive messaging system in the Connect feature, implementing secure encryption using AES-GCM for message content, proper channel management with consent flows, and a well-designed UI separation between sent and received messages with real-time notification integration.

Applied to files:

  • app/src/org/commcare/connect/network/ApiEndPoints.java
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/MessageManager.java
  • app/src/org/commcare/connect/network/ApiPersonalId.java
📚 Learning: 2025-07-29T14:45:13.470Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/connect/MessageManager.java:0-0
Timestamp: 2025-07-29T14:45:13.470Z
Learning: In the CommCare Android Connect messaging system (MessageManager.java), the error handling logic in updateChannelConsent's processFailure callback intentionally attempts to retrieve the channel encryption key even after consent update failure. This behavior was recommended by the server team and the worst case scenario is silently failing to retrieve the encryption key, which is considered acceptable.

Applied to files:

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

Applied to files:

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

Applied to files:

  • app/src/org/commcare/connect/network/ApiEndPoints.java
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/MessageManager.java
  • app/src/org/commcare/connect/network/ConnectSsoHelper.java
  • app/src/org/commcare/connect/network/ApiPersonalId.java
📚 Learning: 2025-05-28T11:30:37.998Z
Learnt from: pm-dimagi
Repo: dimagi/commcare-android PR: 3133
File: app/src/org/commcare/connect/network/connectId/ApiService.java:55-55
Timestamp: 2025-05-28T11:30:37.998Z
Learning: In the CommCare Android codebase, API service method names in ApiService.java should match the format of the actual API endpoint names rather than using semantically meaningful names. For example, if the endpoint is "users/set_recovery_pin", the method name should follow that endpoint structure for consistency and maintainability.

Applied to files:

  • app/src/org/commcare/connect/network/ApiEndPoints.java
  • app/src/org/commcare/connect/network/ApiService.java
  • app/src/org/commcare/connect/network/ApiPersonalId.java
📚 Learning: 2025-01-21T18:20:43.883Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 2912
File: app/src/org/commcare/fragments/connectId/ConnectIdPasswordVerificationFragment.java:206-213
Timestamp: 2025-01-21T18:20:43.883Z
Learning: In the Connect ID feature, server responses containing user data should be parsed using ConnectUserResponseParser to ensure consistent handling of payment information (owner_name and phone_number) and secondary phone verification across all authentication flows.

Applied to files:

  • app/src/org/commcare/connect/network/connectId/parser/LinkHqWorkerResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/MessageManager.java
  • app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt
📚 Learning: 2025-02-04T21:22:56.537Z
Learnt from: pm-dimagi
Repo: dimagi/commcare-android PR: 2949
File: app/src/org/commcare/fragments/connectId/ConnectIdPinFragment.java:244-275
Timestamp: 2025-02-04T21:22:56.537Z
Learning: Direct JSONObject parsing is acceptable for handling user data responses in ConnectIdPinFragment, as decided by the team. No need to enforce ConnectUserResponseParser usage in this context.

Applied to files:

  • app/src/org/commcare/connect/network/connectId/parser/LinkHqWorkerResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt
📚 Learning: 2025-12-10T16:39:05.007Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3448
File: app/src/org/commcare/gis/EntityMapUtils.kt:214-234
Timestamp: 2025-12-10T16:39:05.007Z
Learning: In Kotlin files (here: app/src/org/commcare/gis/EntityMapUtils.kt), follow fail-fast guidance: treat conditions that indicate a programmer error or broken invariant as exceptions rather than defensive handling. If a condition represents a bug (e.g., internal API assumptions like array/list size mismatches), crash early to alert developers instead of attempting to recover. Use clear runtime exceptions (e.g., IllegalStateException, IllegalArgumentException) or assertions where appropriate, and avoid swallowing or masking bugs. Ensure invariants are checked and failures are surfaced with actionable messages for quick debugging.

Applied to files:

  • app/src/org/commcare/connect/network/connectId/parser/LinkHqWorkerResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt
📚 Learning: 2025-12-08T12:01:48.545Z
Learnt from: shubham1g5
Repo: dimagi/commcare-android PR: 3455
File: app/src/org/commcare/connect/network/connectId/parser/RetrieveNotificationsResponseParser.kt:64-93
Timestamp: 2025-12-08T12:01:48.545Z
Learning: In the CommCare Android Connect notification system (RetrieveNotificationsResponseParser.kt), messages are only decrypted using channels that already exist in the database with their encryption keys. When new channels arrive without keys, a separate network flow fetches the channel keys, and then the notification API is re-called to decrypt any messages for those channels. This explains why parseAndSeparateNotifications() correctly uses existingChannels from the database rather than newly parsed channels.

Applied to files:

  • app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/MessageManager.java
  • app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt
  • app/src/org/commcare/connect/network/ApiPersonalId.java
📚 Learning: 2025-04-22T17:04:26.780Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3043
File: app/src/org/commcare/android/database/connect/models/ConnectMessagingChannelRecord.java:0-0
Timestamp: 2025-04-22T17:04:26.780Z
Learning: In the Connect API contract, the server doesn't include the fields `created`, `answered_consent`, and `key` in JSON responses for messaging channels, which is why the `fromJson` method in `ConnectMessagingChannelRecord` explicitly initializes these values rather than parsing them from JSON.

Applied to files:

  • app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/MessageManager.java
  • app/src/org/commcare/connect/network/ApiPersonalId.java
📚 Learning: 2025-04-22T17:04:26.780Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3043
File: app/src/org/commcare/android/database/connect/models/ConnectMessagingChannelRecord.java:0-0
Timestamp: 2025-04-22T17:04:26.780Z
Learning: In the Connect API contract, the server doesn't include the fields `created`, `answered_consent`, and `key` in JSON responses for messaging channels, which is why the `fromJson` method in `ConnectMessagingChannelRecord` explicitly initializes these values rather than parsing them.

Applied to files:

  • app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt
  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/network/ApiPersonalId.java
📚 Learning: 2025-01-21T17:28:09.007Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 2912
File: app/src/org/commcare/android/database/connect/models/ConnectUserRecord.java:66-71
Timestamp: 2025-01-21T17:28:09.007Z
Learning: The ConnectUserRecord class in CommCare Android uses Persisting annotations with sequential indices for field persistence, and MetaField annotations for fields that have corresponding META constants. Fields should be documented with Javadoc comments explaining their purpose, format requirements, and relationships with other fields.

Applied to files:

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

Applied to files:

  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/MessageManager.java
📚 Learning: 2025-04-22T16:44:26.867Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3043
File: app/src/org/commcare/android/database/connect/models/ConnectMessagingChannelRecord.java:86-99
Timestamp: 2025-04-22T16:44:26.867Z
Learning: In ConnectMessagingChannelRecord, fields like META_CHANNEL_ID, META_CONSENT, META_CHANNEL_NAME, and META_KEY_URL are required fields, and the code is intentionally designed to crash if they're missing to raise awareness of data integrity issues rather than handling them silently.

Applied to files:

  • app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
  • app/src/org/commcare/connect/MessageManager.java
  • app/src/org/commcare/connect/network/ApiPersonalId.java
📚 Learning: 2025-01-28T09:38:59.882Z
Learnt from: pm-dimagi
Repo: dimagi/commcare-android PR: 2847
File: app/src/org/commcare/connect/network/connectId/ApiService.java:8-63
Timestamp: 2025-01-28T09:38:59.882Z
Learning: The ConnectID API service methods should use Map<String, String> for request bodies and ResponseBody for responses, as per team preference.

Applied to files:

  • app/src/org/commcare/connect/network/ApiService.java
📚 Learning: 2025-07-29T14:14:07.954Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/adapters/ConnectMessageAdapter.java:0-0
Timestamp: 2025-07-29T14:14:07.954Z
Learning: In the CommCare Android Connect messaging system (ConnectMessageAdapter.java), the team prefers to let getMessage() potentially return null and crash rather than adding defensive null checks. This follows the fail-fast philosophy used throughout the Connect feature to catch programming errors immediately during development.

Applied to files:

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

Applied to files:

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

Applied to files:

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

Applied to files:

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

Applied to files:

  • app/src/org/commcare/connect/MessageManager.java
📚 Learning: 2025-01-21T18:19:05.799Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 2912
File: app/src/org/commcare/fragments/connectId/ConnectIdPasswordVerificationFragment.java:214-215
Timestamp: 2025-01-21T18:19:05.799Z
Learning: In ConnectIdPasswordVerificationFragment, when creating a ConnectUserRecord, it's acceptable for payment information (paymentName and paymentPhone) to be empty strings if the server response doesn't include payment info in the CONNECT_PAYMENT_INFO field.

Applied to files:

  • app/src/org/commcare/connect/MessageManager.java
📚 Learning: 2025-04-22T17:05:39.842Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3043
File: app/src/org/commcare/android/database/connect/models/ConnectMessagingMessageRecord.java:0-0
Timestamp: 2025-04-22T17:05:39.842Z
Learning: In ConnectMessagingMessageRecord, decryption failures are expected in some scenarios and are handled by logging the exception with Logger.exception() but continuing execution by returning null, allowing the application to gracefully handle the failure.

Applied to files:

  • app/src/org/commcare/connect/MessageManager.java
  • app/src/org/commcare/connect/network/ApiPersonalId.java
📚 Learning: 2025-02-04T19:06:14.568Z
Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 2927
File: app/AndroidManifest.xml:151-165
Timestamp: 2025-02-04T19:06:14.568Z
Learning: The HQUserInviteActivity should validate all deep link parameters (callback_url, hq_username, invite_code, hq_domain, connect_username) before processing the invitation. The callback URL should be restricted to trusted domains (commcare.dimagi.com).

Applied to files:

  • app/src/org/commcare/connect/network/ConnectSsoHelper.java
  • app/src/org/commcare/connect/network/ApiPersonalId.java
🧬 Code graph analysis (4)
app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java (6)
app/src/org/commcare/connect/network/connectId/parser/LinkHqWorkerResponseParser.kt (1)
  • context (9-22)
app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt (1)
  • context (12-36)
app/src/org/commcare/connect/network/connectId/parser/RetrieveNotificationsResponseParser.kt (1)
  • context (18-108)
app/src/org/commcare/connect/network/connectId/parser/RetrieveWorkHistoryResponseParser.kt (1)
  • context (13-23)
app/src/org/commcare/connect/network/ApiPersonalId.java (1)
  • ApiPersonalId (52-474)
app/src/org/commcare/connect/network/base/BaseApiHandler.kt (1)
  • createCallback (113-138)
app/src/org/commcare/connect/network/ApiService.java (2)
app/src/org/commcare/connect/network/ApiEndPoints.java (1)
  • ApiEndPoints (3-28)
app/unit-tests/src/org/commcare/connect/network/connectId/parser/ConnectTokenResponseParserTest.kt (1)
  • token (20-24)
app/src/org/commcare/connect/network/ConnectSsoHelper.java (4)
app/src/org/commcare/connect/network/base/BaseApiHandler.kt (1)
  • onFailure (19-22)
app/src/org/commcare/connect/network/connectId/parser/LinkHqWorkerResponseParser.kt (1)
  • context (9-22)
app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt (1)
  • context (17-50)
app/src/org/commcare/connect/network/ApiPersonalId.java (1)
  • ApiPersonalId (52-474)
app/src/org/commcare/connect/network/ApiPersonalId.java (7)
app/src/org/commcare/connect/network/connectId/parser/LinkHqWorkerResponseParser.kt (1)
  • context (9-22)
app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt (1)
  • context (12-36)
app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt (1)
  • context (17-50)
app/src/org/commcare/connect/network/connectId/parser/RetrieveNotificationsResponseParser.kt (1)
  • context (18-108)
app/src/org/commcare/connect/network/connectId/PersonalIdApiClient.java (1)
  • PersonalIdApiClient (10-30)
app/src/org/commcare/connect/network/base/BaseApi.kt (2)
  • callApi (20-89)
  • callApi (23-67)
app/src/org/commcare/connect/network/ApiEndPoints.java (1)
  • ApiEndPoints (3-28)
⏰ 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 (11)
app/src/org/commcare/connect/network/ConnectSsoHelper.java (1)

96-100: linkHqWorker is fire-and-forget; failure won't block token retrieval.

The call to linkHqWorker on line 99 does not wait for completion before proceeding to retrieve the HQ SSO token. If linking fails, the subsequent token retrieval may succeed but the user won't be marked as linked. Verify this is the intended behavior.

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

102-110: LGTM!

The new updateChannelConsent and sendMessagingMessage methods follow the established patterns in this interface, using the team's preferred approach with Map<String, String> for headers and ResponseBody for responses.

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

332-346: LGTM!

The updateChannelConsent method follows the established patterns in this handler class and correctly delegates to ApiPersonalId.updateChannelConsent with appropriate parameters.


365-401: LGTM!

The new methods retrieveChannelEncryptionKey, linkHqWorker, and retrieveHqTokenASync follow consistent patterns with appropriate parser delegation. The integration with the new response parsers (RetrieveChannelEncryptionKeyResponseParser, LinkHqWorkerResponseParser, RetrieveHqTokenResponseParser) aligns with the broader refactoring in this PR.

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

12-12: LGTM on import update.

The import change from ApiPersonalId and IApiCallback to PersonalIdApiHandler aligns with the refactoring to the handler-based pattern.


32-52: LGTM on updateChannelConsent refactoring.

The migration to PersonalIdApiHandler<Boolean> is clean. The success path correctly stores the channel and conditionally fetches the encryption key based on consent status. Based on learnings, the behavior of fetching the encryption key even in some failure scenarios was intentional per server team guidance—this updated flow simplifies by only fetching on success, which seems like an intentional design change.

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

148-165: LGTM on linkHqWorker async implementation.

The refactoring to Retrofit-based calls with makePostRequest is well-structured. The Objects.requireNonNull(tokenAuth) on line 158 provides fail-fast behavior if credentials are invalid.


175-180: Wrapping MalformedURLException as RuntimeException is acceptable here.

Since ServerUrls.getKeyServer() returns a configured URL, a malformed URL indicates a serious configuration error. Failing fast with a RuntimeException is appropriate.


381-396: LGTM on updateChannelConsent migration to Retrofit.

The refactoring to use ApiService.updateChannelConsent with proper header construction and callback-based handling is clean and consistent with the other async methods.


417-441: LGTM on retrieveChannelEncryptionKey Retrofit migration.

The token callback pattern is preserved while the internal implementation now uses the Retrofit-based makePostRequest. Token unavailability and denial are properly propagated via the callback interface.


443-473: LGTM on sendMessagingMessage Retrofit migration.

The method correctly builds the encrypted message payload and delegates to the Retrofit-based sendMessagingMessage endpoint. The error handling for encryption failures (lines 460-462) logs the exception appropriately.

Comment on lines +26 to +34
if (responseAsString.isNotEmpty()) {
val json = JSONObject(responseAsString)
channel.setKey(json.getString("key"))
ConnectMessagingDatabaseHelper.storeMessagingChannel(context, channel)
}
} catch (e: JSONException) {
throw RuntimeException(e)
}
return true as T
Copy link

@coderabbitai coderabbitai bot Jan 6, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty response silently succeeds without updating the channel key.

When responseAsString.isEmpty(), the method returns true without setting the key or persisting the channel. This could mask a server issue returning an empty body. Consider whether this should be treated as an error.

Additionally, StreamsUtil.inputStreamToByteArray() can throw IOException which is not caught here, unlike JSONException.

🔎 Suggested improvement
+import java.io.IOException
+
 class RetrieveChannelEncryptionKeyResponseParser<T>(
     val context: Context,
 ) : BaseApiResponseParser<T> {
     override fun parse(
         responseCode: Int,
         responseData: InputStream,
         anyInputObject: Any?,
     ): T {
         try {
             val channel = anyInputObject as ConnectMessagingChannelRecord
             val responseAsString =
                 String(
                     StreamsUtil.inputStreamToByteArray(responseData),
                 )
             if (responseAsString.isNotEmpty()) {
                 val json = JSONObject(responseAsString)
                 channel.setKey(json.getString("key"))
                 ConnectMessagingDatabaseHelper.storeMessagingChannel(context, channel)
+            } else {
+                throw RuntimeException("Empty response received for channel encryption key")
             }
-        } catch (e: JSONException) {
+        } catch (e: Exception) {
+            when (e) {
+                is JSONException, is IOException -> throw RuntimeException(e)
+                else -> throw e
+            }
-            throw RuntimeException(e)
         }
         return true as T
     }
 }
🤖 Prompt for AI Agents
In
@app/src/org/commcare/connect/network/connectId/parser/RetrieveChannelEncryptionKeyResponseParser.kt
around lines 26 - 34, The parser currently treats an empty HTTP body as a
successful response and only catches JSONException; update retrieve logic in
RetrieveChannelEncryptionKeyResponseParser (the block that reads
responseAsString, parses JSONObject, calls channel.setKey(...) and
ConnectMessagingDatabaseHelper.storeMessagingChannel(...)) so that an empty
response is treated as an error (e.g., throw a RuntimeException or return a
failure rather than returning true) and ensure
StreamsUtil.inputStreamToByteArray() IOExceptions are caught and handled the
same way as the JSONException so missing/empty bodies or IO failures don’t
silently succeed.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems worth logging (or even crashing) if encountered as it should never happen

Copy link

Choose a reason for hiding this comment

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

@OrangeAndGreen, agreed! Since an empty response represents a data contract violation (the server should always return a key field), it should fail-fast with a RuntimeException rather than silently succeeding.

Based on the established error-handling pattern in Connect code (per the learning from ConnectUnlockFragment), I'd suggest:

  • Empty response: Throw RuntimeException with a descriptive message (data contract violation)
  • JSONException: Continue throwing RuntimeException (already does this - data contract violation)
  • IOException from StreamsUtil.inputStreamToByteArray(): Log the error and either return a failure indicator or throw, depending on whether you consider stream reading failures as recoverable

Would you like me to plan the code changes for this fix? If so, should IOException be handled gracefully (logged but not crashed), or should it also crash since it prevents us from reading the encryption key?


🧠 Learnings used
Learnt from: shubham1g5
Repo: dimagi/commcare-android PR: 3455
File: app/src/org/commcare/connect/network/connectId/parser/RetrieveNotificationsResponseParser.kt:64-93
Timestamp: 2025-12-08T12:01:48.545Z
Learning: In the CommCare Android Connect notification system (RetrieveNotificationsResponseParser.kt), messages are only decrypted using channels that already exist in the database with their encryption keys. When new channels arrive without keys, a separate network flow fetches the channel keys, and then the notification API is re-called to decrypt any messages for those channels. This explains why parseAndSeparateNotifications() correctly uses existingChannels from the database rather than newly parsed channels.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/connect/MessageManager.java:0-0
Timestamp: 2025-07-29T14:45:13.470Z
Learning: In the CommCare Android Connect messaging system (MessageManager.java), the error handling logic in updateChannelConsent's processFailure callback intentionally attempts to retrieve the channel encryption key even after consent update failure. This behavior was recommended by the server team and the worst case scenario is silently failing to retrieve the encryption key, which is considered acceptable.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 2912
File: app/src/org/commcare/fragments/connectId/ConnectIdPasswordVerificationFragment.java:206-213
Timestamp: 2025-01-21T18:20:43.883Z
Learning: In the Connect ID feature, server responses containing user data should be parsed using ConnectUserResponseParser to ensure consistent handling of payment information (owner_name and phone_number) and secondary phone verification across all authentication flows.

Learnt from: pm-dimagi
Repo: dimagi/commcare-android PR: 2949
File: app/src/org/commcare/fragments/connectId/ConnectIdPinFragment.java:244-275
Timestamp: 2025-02-04T21:22:56.537Z
Learning: Direct JSONObject parsing is acceptable for handling user data responses in ConnectIdPinFragment, as decided by the team. No need to enforce ConnectUserResponseParser usage in this context.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3043
File: app/src/org/commcare/android/database/connect/models/ConnectMessagingChannelRecord.java:0-0
Timestamp: 2025-04-22T17:04:26.780Z
Learning: In the Connect API contract, the server doesn't include the fields `created`, `answered_consent`, and `key` in JSON responses for messaging channels, which is why the `fromJson` method in `ConnectMessagingChannelRecord` explicitly initializes these values rather than parsing them from JSON.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3043
File: app/src/org/commcare/android/database/connect/models/ConnectMessagingChannelRecord.java:0-0
Timestamp: 2025-04-22T17:04:26.780Z
Learning: In the Connect API contract, the server doesn't include the fields `created`, `answered_consent`, and `key` in JSON responses for messaging channels, which is why the `fromJson` method in `ConnectMessagingChannelRecord` explicitly initializes these values rather than parsing them.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3043
File: app/src/org/commcare/android/database/connect/models/ConnectMessagingChannelRecord.java:86-99
Timestamp: 2025-04-22T16:44:26.867Z
Learning: In ConnectMessagingChannelRecord, fields like META_CHANNEL_ID, META_CONSENT, META_CHANNEL_NAME, and META_KEY_URL are required fields, and the code is intentionally designed to crash if they're missing to raise awareness of data integrity issues rather than handling them silently.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java:0-0
Timestamp: 2025-07-29T14:08:55.466Z
Learning: In ConnectUnlockFragment.java and similar Connect-related code, the team has agreed that JSONExceptions should crash the app by throwing RuntimeException (fail-fast approach for data contract violations), while IOExceptions should be logged and allow processing to continue (graceful handling for network/stream issues). This intentional inconsistency in error handling reflects different treatment for different types of failures.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3043
File: app/src/org/commcare/android/database/connect/models/ConnectMessagingMessageRecord.java:0-0
Timestamp: 2025-04-22T17:05:39.842Z
Learning: In ConnectMessagingMessageRecord, decryption failures are expected in some scenarios and are handled by logging the exception with Logger.exception() but continuing execution by returning null, allowing the application to gracefully handle the failure.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3108
File: app/src/org/commcare/activities/StandardHomeActivityUIController.java:0-0
Timestamp: 2025-06-20T13:21:20.908Z
Learning: User OrangeAndGreen prefers to handle code issues in separate PRs rather than immediately fixing them in the current PR when they are not directly related to the main changes.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/connect/network/ApiPersonalId.java:0-0
Timestamp: 2025-07-29T14:56:40.681Z
Learning: User OrangeAndGreen prefers to defer ApiPersonalId refactoring (specifically InputStream resource management improvements) to separate PRs rather than mixing them with Connect feature work, even when the code is already in master.

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

Learnt from: Jignesh-dimagi
Repo: dimagi/commcare-android PR: 0
File: :0-0
Timestamp: 2025-05-08T13:40:19.645Z
Learning: PR #3048 introduces a comprehensive messaging system in the Connect feature, implementing secure encryption using AES-GCM for message content, proper channel management with consent flows, and a well-designed UI separation between sent and received messages with real-time notification integration.

Learnt from: OrangeAndGreen
Repo: dimagi/commcare-android PR: 3248
File: app/src/org/commcare/connect/MessageManager.java:0-0
Timestamp: 2025-07-29T14:54:47.940Z
Learning: User OrangeAndGreen organizes messaging-related changes into separate PRs rather than mixing them with other Connect work, which aligns with their preference for handling code issues in separate PRs when they are not directly related to the main changes.

val seconds = if (json.has(key)) json.getInt(key) else 0
expiration.setTime(expiration.getTime() + (seconds.toLong() * 1000))

val seatedAppId = CommCareApplication.instance().getCurrentApp().getUniqueId()
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

🏁 Script executed:

#!/bin/bash
# First, check how getCurrentApp() is used in the codebase
rg -n "getCurrentApp\(\)" --type kotlin --type java -C2 | head -100

Repository: dimagi/commcare-android

Length of output: 11601


🏁 Script executed:

#!/bin/bash
# Find the CommCareApplication class to check getCurrentApp() signature
fd -t f "CommCareApplication\." --type f | head -10
rg -n "fun getCurrentApp" --type kotlin --type java -B2 -A5

Repository: dimagi/commcare-android

Length of output: 112


🏁 Script executed:

#!/bin/bash
# Check the specific file mentioned in the review
cat -n app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt | sed -n '35,50p'

Repository: dimagi/commcare-android

Length of output: 819


🏁 Script executed:

#!/bin/bash
# Find getCurrentApp() method definition
rg -n "fun getCurrentApp|public.*getCurrentApp" --type kotlin --type java -A3

Repository: dimagi/commcare-android

Length of output: 1389


🏁 Script executed:

#!/bin/bash
# Also check CommCareApplication.java for getCurrentApp()
rg -n "getCurrentApp" app/src/org/commcare/CommCareApplication.java -B2 -A5

Repository: dimagi/commcare-android

Length of output: 4113


Add null check before calling getUniqueId().

CommCareApplication.instance().getCurrentApp() can return null (see defensive null checks in TestUtils), but line 41 calls .getUniqueId() directly without a null check, risking an NPE if no app is currently seated.

Example fix
val currentApp = CommCareApplication.instance().getCurrentApp()
if (currentApp != null) {
    val seatedAppId = currentApp.getUniqueId()
    val ssoToken = SsoToken(token, expiration)
    ConnectDatabaseHelper.storeHqToken(context, seatedAppId, hqUsername, ssoToken)
} else {
    // Handle case when no app is seated
}
🤖 Prompt for AI Agents
In
@app/src/org/commcare/connect/network/connectId/parser/RetrieveHqTokenResponseParser.kt
at line 41, CommCareApplication.instance().getCurrentApp() can be null, so
before calling getUniqueId() in RetrieveHqTokenResponseParser (where you create
seatedAppId and then call SsoToken and ConnectDatabaseHelper.storeHqToken),
fetch currentApp into a local variable, check it for null, and only call
currentApp.getUniqueId() and store the token when non-null; in the else branch
handle the no-app-seated case (e.g., log an error via the parser's logger and
return/skip storing) so you avoid an NPE.

Comment on lines 348 to 362
public void sendMessagingMessage(
Context context,
ConnectUserRecord user,
ConnectMessagingMessageRecord messsage,
ConnectMessagingChannelRecord channel
) {
ApiPersonalId.sendMessagingMessage(
context,
user.getUserId(),
user.getPassword(),
messsage,
channel.getKey(),
createCallback(new NoParsingResponseParser<>(), null)
);
}
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 parameter name: messsage should be message.

🔎 Suggested fix
     public void sendMessagingMessage(
             Context context,
             ConnectUserRecord user,
-            ConnectMessagingMessageRecord messsage,
+            ConnectMessagingMessageRecord message,
             ConnectMessagingChannelRecord channel
     ) {
         ApiPersonalId.sendMessagingMessage(
                 context,
                 user.getUserId(),
                 user.getPassword(),
-                messsage,
+                message,
                 channel.getKey(),
                 createCallback(new NoParsingResponseParser<>(), null)
         );
     }
📝 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 sendMessagingMessage(
Context context,
ConnectUserRecord user,
ConnectMessagingMessageRecord messsage,
ConnectMessagingChannelRecord channel
) {
ApiPersonalId.sendMessagingMessage(
context,
user.getUserId(),
user.getPassword(),
messsage,
channel.getKey(),
createCallback(new NoParsingResponseParser<>(), null)
);
}
public void sendMessagingMessage(
Context context,
ConnectUserRecord user,
ConnectMessagingMessageRecord message,
ConnectMessagingChannelRecord channel
) {
ApiPersonalId.sendMessagingMessage(
context,
user.getUserId(),
user.getPassword(),
message,
channel.getKey(),
createCallback(new NoParsingResponseParser<>(), null)
);
}
🤖 Prompt for AI Agents
In @app/src/org/commcare/connect/network/connectId/PersonalIdApiHandler.java
around lines 348 - 362, Rename the misspelled parameter `messsage` to `message`
in the method signature of sendMessagingMessage and update all references inside
the method body (e.g., the argument passed to
ApiPersonalId.sendMessagingMessage) to use the corrected name; ensure the
parameter type remains ConnectMessagingMessageRecord and no other behavior
changes are introduced.

Copy link
Contributor Author

@Jignesh-dimagi Jignesh-dimagi Jan 8, 2026

Choose a reason for hiding this comment

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

corrected here

Comment on lines 261 to 266
public void onFailure(@NonNull PersonalIdOrConnectApiErrorCodes errorCode, @Nullable Throwable t) {
if(t!=null){
Logger.exception("Failed to link HQ workder ", t);
}else{
Logger.exception("Failed to link HQ workder", new Throwable("Failed to link HQ workder"));
}
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 log messages: "workder" should be "worker".

🔎 Suggested fix
         @Override
         public void onFailure(@NonNull PersonalIdOrConnectApiErrorCodes errorCode, @Nullable Throwable t) {
             if(t!=null){
-                Logger.exception("Failed to link HQ workder ", t);
+                Logger.exception("Failed to link HQ worker", t);
             }else{
-                Logger.exception("Failed to link HQ workder", new Throwable("Failed to link HQ workder"));
+                Logger.exception("Failed to link HQ worker", new Throwable("Failed to link HQ worker"));
             }
         }
📝 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 onFailure(@NonNull PersonalIdOrConnectApiErrorCodes errorCode, @Nullable Throwable t) {
if(t!=null){
Logger.exception("Failed to link HQ workder ", t);
}else{
Logger.exception("Failed to link HQ workder", new Throwable("Failed to link HQ workder"));
}
public void onFailure(@NonNull PersonalIdOrConnectApiErrorCodes errorCode, @Nullable Throwable t) {
if(t!=null){
Logger.exception("Failed to link HQ worker", t);
}else{
Logger.exception("Failed to link HQ worker", new Throwable("Failed to link HQ worker"));
}
🤖 Prompt for AI Agents
In @app/src/org/commcare/connect/network/ConnectSsoHelper.java around lines 261
- 266, Fix the typo "workder" to "worker" in the failure logging inside the
onFailure callback in ConnectSsoHelper: update both Logger.exception calls (the
one logging the Throwable t and the one constructing a new Throwable) so the
message reads "Failed to link HQ worker" and keep existing Throwable usage and
context intact.

Copy link
Contributor Author

@Jignesh-dimagi Jignesh-dimagi Jan 8, 2026

Choose a reason for hiding this comment

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

corrected here

Copy link
Contributor

@conroy-ricketts conroy-ricketts left a comment

Choose a reason for hiding this comment

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

Looking good, just had minor feedback. Think we should also at least address the typos that Coderabbit pointed out


public static void retrieveHqTokenASync(Context context, String hqUsername, String connectToken,IApiCallback callback) {
HashMap<String, Object> params = new HashMap<>();
params.put("client_id", "4eHlQad1oasGZF0lPiycZIjyL0SY1zx7ZblA6SCV");
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we move the client ID to a constant variable

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, just to confirm, should this ID always be the same?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is used in this HQ token API only, so not putting in constant.

Copy link
Contributor

Choose a reason for hiding this comment

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

Understandable, I was thinking that having it as a constant like CONNECT_CLIENT_ID currently is at the top of this class would make for easy visibility. But I'm fine with this being optional

@conroy-ricketts
Copy link
Contributor

Also, should we include unit tests with these new parser classes?

@shubham1g5 As a broader discussion, should the team be required to write new unit tests when applicable for new changes? Can't remember if we had already discussed this. I know I've personally been slacking a bit when it comes to this

Copy link
Contributor

@shubham1g5 shubham1g5 left a comment

Choose a reason for hiding this comment

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

The formatting on new code is off at several places and lots of un-needed spacing between lines which should be corrected before merge.

Comment on lines 120 to 122
// This is created if needs to be used in future like above post method
@GET
Call<ResponseBody> makeGetRequest(
Copy link
Contributor

Choose a reason for hiding this comment

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

we should not be creating any unused methods that we think might get used in future, in general PRs should not have any unused code unless you soon plan to do another PR as a followup to this using the corresponding block of code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed here

Comment on lines 423 to 427
HashMap<String, String> headers = new HashMap<>();
String token = HttpUtils.getCredential(tokenAuth);
RequestBody requestBody = ConnectNetworkHelper.buildPostFormHeaders(params, true, PersonalIdApiClient.API_VERSION, headers);
ApiService apiService = PersonalIdApiClient.getClientApi();
Call<ResponseBody> call = apiService.makePostRequest(channelUrl,token, headers,requestBody);
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like we are repating this code at various places in this PR and should be abstracted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, as AuthInfo is different for some URLs, I kept separate in their corresponding methods only.

Copy link
Contributor

Choose a reason for hiding this comment

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

as AuthInfo is different for some URLs

Would you be able to share more on this ? I am not sure I get why that will be an issue here given it has a common superclass.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@shubham1g5 Sorry, I initially mis-understand it, changed now here.

}
@Override
public void onFailure(PersonalIdOrConnectApiErrorCodes failureCode, Throwable t) {
Logger.log("Messaging", "Failed to retreive encryption key: " + (t!=null ? t.getMessage():""));
Copy link
Contributor

Choose a reason for hiding this comment

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

typo as flagged by code rabbit below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

ConnectMessagingChannelRecord channel = ConnectMessagingDatabaseHelper.getMessagingChannel(context, message.getChannelId());

if (channel.getKey().length() > 0) {
if (channel!=null && !channel.getKey().isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

how can channel be null here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, removed here

if (linkHqUser || appRecord.getWorkerLinked()) {

// first get the connect token
new PersonalIdApiHandler<AuthInfo.TokenAuth>() {
Copy link
Contributor

Choose a reason for hiding this comment

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

can we abstract this into another method, the method seems to run super long already with the top level network call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

public void onSuccess(AuthInfo.TokenAuth hqToken) {
callback.tokenRetrieved(hqToken);
}
}.retrieveHqTokenASync(context, hqUsername,connectIdToken.bearerToken);
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a reason we are adding ASync suffix here when we are not doing so with other similar network methods in PersonalIdApiHandler. I am confused is there is something different about these Async methods in comparison to other ApiPersonalId calls

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No different than other ApiPersonalId calls so removed here



}
}.connectToken(context, user);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can rename to retrievePersonalIdToken to be consistent with underlying ApiPersonalId call

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

if(hqTokenAuth != null) {
return hqTokenAuth;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Copy link
Contributor

Choose a reason for hiding this comment

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

we already require Android N for signing up for Personal ID, as such think it's safe to remove these checks and delete anything connect/personal ID related that is there to support devices < Android N

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@shubham1g5 This is really excellent point. I will remove all such checking for Android N. Thanks for flagging this.

@shubham1g5
Copy link
Contributor

As a broader discussion, should the team be required to write new unit tests when applicable for new changes? Can't remember if we had already discussed this. I know I've personally been slacking a bit when it comes to this

@conroy-ricketts Good callout here, I don't think we have concrete expectations there yet, think I would expect tests for components that already have tests like parsers for example although think we should be calling this out in Jira tickets explicitly as these tests will tend to increase scope of the tickets. I don't think we did it for this ticket, so would leave it to @Jignesh-dimagi based on the capacity he have in the Sprint to tackle tests on it, otherwise think we should create a separate ticket for the new parsers that we introduced in this PR.

@Jignesh-dimagi
Copy link
Contributor Author

@conroy-ricketts Good callout here, I don't think we have concrete expectations there yet, think I would expect tests for components that already have tests like parsers for example although think we should be calling this out in Jira tickets explicitly as these tests will tend to increase scope of the tickets. I don't think we did it for this ticket, so would leave it to @Jignesh-dimagi based on the capacity he have in the Sprint to tackle tests on it, otherwise think we should create a separate ticket for the new parsers that we introduced in this PR.

@shubham1g5 @conroy-ricketts I was also with view that parsers should have unit tests and was going to implement the same but this time my sprint is not having good bandwidth to attempt it as this ticket has taken some extra time to clean up so lean towards separate ticket for the same if it's okay to you guys.

Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed that the JIRA ticket mentioned moving the few remaining helper methods out of ConnectNetworkHelper making it effectively obsolete (and I assume we should be ale to delete this class entirely). Is this something we still want to do here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ConnectNetworkHelper's network call methods are obsolete and removed, now it has only abstract methods to build the request.

conroy-ricketts
conroy-ricketts previously approved these changes Jan 9, 2026
Copy link
Contributor

@shubham1g5 shubham1g5 left a comment

Choose a reason for hiding this comment

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

Can we specify Safety Story for PR based on dev testing you have done for the PR. think it's critical for us to test the major SSO related flows before merging this in, specially things like "Syncing forms inside the HQ app with a Personal ID login"

}


public static void retrieveConnectIdTokenAsync(Context context, @NonNull ConnectUserRecord user, TokenCallback callback) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should rename to retrievePersonalIdToken as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done here

@Jignesh-dimagi
Copy link
Contributor Author

Can we specify Safety Story for PR based on dev testing you have done for the PR. think it's critical for us to test the major SSO related flows before merging this in, specially things like "Syncing forms inside the HQ app with a Personal ID login"

I have already added points in QA Plan but also added your last point.

@Jignesh-dimagi
Copy link
Contributor Author

Linking HQ and Connect

Think we need to verify this pre-merge as this is what SSO changes affects.

Yes we will need QA to verify as mentioned in description that with my account I am getting the same error with master branch also.

@Jignesh-dimagi
Copy link
Contributor Author

Form submission
Syncing forms for HQ

It's critical for the PR to test things like we are able to send forms on logging in with a Personal ID account, are you able to confirm the same ?

Yes it's working from my side.

@shubham1g5
Copy link
Contributor

Yes we will need QA to verify as mentioned in description that with my account I am getting the same error with master branch also.

Are you able to dig into why that's happening, we are regularly deploying test betas from master now for field testing and can't rely on release QA to test things which should be tested by devs originally as part of their work.

@Jignesh-dimagi
Copy link
Contributor Author

Yes we will need QA to verify as mentioned in description that with my account I am getting the same error with master branch also.

Are you able to dig into why that's happening, we are regularly deploying test betas from master now for field testing and can't rely on release QA to test things which should be tested by devs originally as part of their work.

Ok sure. Should I create a separate ticket for the same as this is the same without this PR changes also.

@shubham1g5
Copy link
Contributor

Ok sure. Should I create a separate ticket for the same as this is the same without this PR changes also.

I don't think there is a platform bug here, and it might be related to just your account/dev setup. Are you able to spend a couple hours looking into it so that we have more info on this before creating a ticket on to why it's happening.

@Jignesh-dimagi
Copy link
Contributor Author

I don't think there is a platform bug here, and it might be related to just your account/dev setup. Are you able to spend a couple hours looking into it so that we have more info on this before creating a ticket on to why it's happening.

Yeah, It tried debugging before raising this PR but as server is responding with 500, no more other information. I guess my account is already linked so giving this error and that's why I was proposing QA here to find out more.

Copy link
Contributor

@OrangeAndGreen OrangeAndGreen left a comment

Choose a reason for hiding this comment

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

Might be worth running the code formatter to address spacing nits. Otherwise just one noteworthy concern about async behavior when linking PersonalID-HQ

Comment on lines +26 to +34
if (responseAsString.isNotEmpty()) {
val json = JSONObject(responseAsString)
channel.setKey(json.getString("key"))
ConnectMessagingDatabaseHelper.storeMessagingChannel(context, channel)
}
} catch (e: JSONException) {
throw RuntimeException(e)
}
return true as T
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems worth logging (or even crashing) if encountered as it should never happen

}

public static void linkHqWorker(Context context, String hqUsername, ConnectLinkedAppRecord appRecord, String connectToken) {
public static void linkHqWorker(Context context, String hqUsername, ConnectLinkedAppRecord appRecord, String connectToken,IApiCallback callback) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Missing space "connectToken,IApiCallback"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here


public static AuthInfo.TokenAuth retrieveHqTokenSync(Context context, String hqUsername, String connectToken)
throws TokenUnavailableException {
public static void retrieveHqTokenASync(Context context, String hqUsername, String connectToken,IApiCallback callback) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can get rid of the "ASync" in function name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here


public static AuthInfo.TokenAuth retrieveHqTokenSync(Context context, String hqUsername, String connectToken)
throws TokenUnavailableException {
public static void retrieveHqTokenASync(Context context, String hqUsername, String connectToken,IApiCallback callback) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Missing space "connectToken,IApiCallback"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

Copy link
Contributor

Choose a reason for hiding this comment

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

This can be deleted now, no longer used

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

IApiCallback callback){
RequestBody requestBody = ConnectNetworkHelper.buildPostFormHeaders(params, useFormEncoding, PersonalIdApiClient.API_VERSION, headers);
ApiService apiService = PersonalIdApiClient.getClientApi();
Call<ResponseBody> call = apiService.makePostRequest(channelUrl,token, headers,requestBody);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: spacing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

RequestBody requestBody = ConnectNetworkHelper.buildPostFormHeaders(params, false, PersonalIdApiClient.API_VERSION, headers);
ApiService apiService = PersonalIdApiClient.getClientApi();
Call<ResponseBody> call = apiService.sendMessagingMessage(tokenAuth, headers,requestBody);
BaseApi.Companion.callApi(context, call, callback,ApiEndPoints.CONNECT_MESSAGE_SEND_URL);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: spacing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

}
@Override
public void onSuccess(AuthInfo.TokenAuth connectIdToken) {
linkHqUser(context,appRecord,hqUsername,connectIdToken);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this may not work correctly since linkHqWorker is asynchronous and retrieveHqToken requires the link to be configured in order to succeed (we shouldn't try to retrieve a token until performing the link succeeds)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@OrangeAndGreen Yeah thats good catch. Thanks.
Previous code was not checking if linking HQ worker was successful or not. So with this new change, we should try to retrieve token irrespective of linking HQ worker success/failure as server might return error if already linked previously, your thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, that's a good point. I think the behavior you described (server returning an error if user is already linked) used to be the case but that was changed at some point. Now the server should simply succeed the call (without making any changes) when the user is already linked.
But if we want to be safe and maintain the existing behavior (trying to proceed to token retrieval even when the linking call fails) that should be fine. If the linking call fails for good reason the subsequent call to retrieve token will fail anyway, so the end result for the user is the same.
The important thing is that we give the linking call a chance to succeed before we make the token call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

if (!appRecord.getWorkerLinked()) {
//Link user if necessary
ApiPersonalId.linkHqWorker(context, hqUsername, appRecord, connectIdToken.bearerToken);
@RequiresApi(api = Build.VERSION_CODES.N)
Copy link
Contributor

Choose a reason for hiding this comment

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

Noting that this API requirement creates a compiler warning/error in PersonalIdManager.getHqTokenIfLinked

Copy link
Contributor Author

@Jignesh-dimagi Jignesh-dimagi Jan 19, 2026

Choose a reason for hiding this comment

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

Yeah, it was assumed that PersonalId / Connect is restricted to Android N only but I have changed now to remove that that OS specific requirements. Done here

//Link user if necessary
ApiPersonalId.linkHqWorker(context, hqUsername, appRecord, connectIdToken.bearerToken);
@RequiresApi(api = Build.VERSION_CODES.N)
public static AuthInfo.TokenAuth retrieveHqSsoTokenSync(Context context, @NonNull ConnectUserRecord user, @NonNull ConnectLinkedAppRecord appRecord, String hqUsername, boolean performLink) throws
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: long line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done here

Copy link
Contributor

@OrangeAndGreen OrangeAndGreen left a comment

Choose a reason for hiding this comment

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

Left a couple questions to better understand the exception handling.
Also heads up that it looks like the formatter should be run one more time, at least for ConnectSsoHelper

}

override fun tokenUnavailable() {
// Cannot raise exception as not handled here
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like the exception is expected to rise through CommCareRequestGenerator.buildAuth (note that method declares that it throws the two token exceptions). So it seems the relevant exceptions should be thrown from here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking of not raising the exception but handling token = null here. But as this CommCareRequestGenerator.buildAuth is also raising these exceptions, I agree with your point and added back the exception again.

Copy link
Contributor

@conroy-ricketts conroy-ricketts left a comment

Choose a reason for hiding this comment

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

LGTM pending Dave's latest comments

OrangeAndGreen
OrangeAndGreen previously approved these changes Jan 21, 2026
appRecord: ConnectLinkedAppRecord,
hqUsername: String?,
performLink: Boolean,
): AuthInfo.TokenAuth? =
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "AuthInfo." not necessary here, TokenAuth is imported directly

appRecord: ConnectLinkedAppRecord,
hqUsername: String?,
performLink: Boolean,
): AuthInfo.TokenAuth? =
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "AuthInfo." not necessary here, TokenAuth is imported directly

@Jignesh-dimagi
Copy link
Contributor Author

@shubham1g5 It looks like the review is done. As you have offered help here for testing, can you please do that? @OrangeAndGreen @conroy-ricketts I have already done one round from my side, as mentioned in the ticket description, but if anyone has bandwidth to just test it once, as it's a very crucial part change

@conroy-ricketts
Copy link
Contributor

I have already done one round from my side, as mentioned in the ticket description, but if anyone has bandwidth to just test it once, as it's a very crucial part change

@Jignesh-dimagi Yeah sure thing, I will try as soon as I can (hopefully later today) and let you know

@OrangeAndGreen
Copy link
Contributor

@Jignesh-dimagi Just noticed there are some conflicts to resolve here.

@shubham1g5
Copy link
Contributor

@Jignesh-dimagi I was able to test the SSO auth with HQ here and submit a form successfully with Personal ID login.

@Jignesh-dimagi
Copy link
Contributor Author

@Jignesh-dimagi I was able to test the SSO auth with HQ here and submit a form successfully with Personal ID login.

Thanks @shubham1g5 for testing

@Jignesh-dimagi
Copy link
Contributor Author

@Jignesh-dimagi Just noticed there are some conflicts to resolve here.

@OrangeAndGreen updated the base branch, thanks

@Jignesh-dimagi Jignesh-dimagi merged commit 450baf6 into master Jan 26, 2026
9 checks passed
@Jignesh-dimagi Jignesh-dimagi deleted the jignesh/feat/ccct-1888 branch January 26, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants