Skip to content

CCCT-2070 - Connect Generic Opportunity Push Notification#3555

Open
Jignesh-dimagi wants to merge 23 commits intomasterfrom
ccct-2070-generic-oppor-pn-2
Open

CCCT-2070 - Connect Generic Opportunity Push Notification#3555
Jignesh-dimagi wants to merge 23 commits intomasterfrom
ccct-2070-generic-oppor-pn-2

Conversation

@Jignesh-dimagi
Copy link
Contributor

@Jignesh-dimagi Jignesh-dimagi commented Feb 20, 2026

Product Description

This will have changes in push notifications, as the app is now able to show all the Connect generic opportunity push notifications.

As some of the server changes were not available, the UUID ticket was not merged in master, but it was required for these changes, so it was merged in this ticket.

Some of the changes which are not present in the UUID ticket but are available in master are showing in diff, but this will go away whenever the UUID ticket is merged in master and this PR's base branch is changed to master.

Technical Summary

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

From now on, the application will handle all types of Connect generic opportunity push notifications. Push notification having ccc_generic_opportunity as an action is considered a Connect generic opportunity. This type of push notification will be used whenever it's required to

  • Show the learn/delivery progress screen

  • Show payment screen

The decision to show learning/delivery is taken by mobile code, and right now, no forceful action is calculated from the push notification payload.

Regarding Firebase Analytics, if the push notification is this generic, it will log the action available in its payload as `key 'e.g.'

Safety Assurance

Safety story

I have tested the payment rollback available on staging server and its working for the mobile.

QA Plan

Labels and Review

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

@Jignesh-dimagi
Copy link
Contributor Author

@damagatchi retest this please

@Jignesh-dimagi Jignesh-dimagi changed the base branch from jignesh/feat/ccct-2054 to master February 20, 2026 13:16
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

This pull request migrates the Android Connect platform from integer-based identifiers (jobId, unitId, paymentId) to UUID-based string identifiers across the entire data layer and API surface. Database models (ConnectJobRecord, ConnectDeliveryDetails, ConnectJobPaymentRecord, etc.) are updated to use UUID fields. Database helper methods (ConnectJobUtils) change to query by UUID instead of numeric IDs. Network API endpoints (ApiConnect, ApiService) are modified to accept and pass UUIDs. Activities and fragments are updated to use the new UUID-based getters and setters. A new generic opportunity notification type (CCC_GENERIC_OPPORTUNITY) is introduced with centralized action resolution utilities (getActionFromIntent, getActionFromRecord, getActionFromPayload). Database upgrade paths are added to handle schema evolution. Notification sync workers are enhanced to support the generic opportunity flow alongside existing delivery and learning progress synchronization.

Sequence Diagram

sequenceDiagram
    participant App as Activities/Fragments
    participant NotiHandler as NotificationsSyncWorkerManager
    participant NotiWorker as NotificationsSyncWorker
    participant JobHelper as ConnectJobHelper
    participant DBUtil as ConnectJobUtils
    participant API as ApiConnect/ApiService
    participant DB as Database

    App->>NotiHandler: Notification received (CCC_GENERIC_OPPORTUNITY + OPPORTUNITY_UUID)
    NotiHandler->>NotiHandler: Identify as generic opportunity
    NotiHandler->>NotiWorker: Enqueue SYNC_GENERIC_OPPORTUNITY work
    
    NotiWorker->>NotiWorker: startAppropriateSync()
    NotiWorker->>DBUtil: getCompositeJob(context, opportunityUUID)
    DBUtil->>DB: Query by META_JOB_UUID
    DB-->>DBUtil: Return ConnectJobRecord
    DBUtil-->>NotiWorker: ConnectJobRecord
    
    alt Job found
        NotiWorker->>JobHelper: updateJobProgress(job, listener)
        JobHelper->>API: Fetch delivery/learning progress
        API-->>JobHelper: Updated data
        JobHelper->>DB: Store updated deliveries/learnings/assessments
        DB-->>JobHelper: Success
        JobHelper->>NotiWorker: Progress updated
        NotiWorker-->>App: Sync complete
    else Job not found
        NotiWorker->>NotiWorker: handleNoConnectJob()
        NotiWorker-->>App: Sync failed (no job)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'CCCT-2070 - Connect Generic Opportunity Push Notification' clearly summarizes the main change: adding support for handling generic opportunity push notifications in the Connect app.
Description check ✅ Passed The PR description includes all required template sections: Product Description, Technical Summary (with linked ticket), Safety Assurance (safety story and QA plan), and review checklist items.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ccct-2070-generic-oppor-pn-2

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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
Contributor

@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: 6

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/activities/DispatchActivity.java (1)

170-178: ⚠️ Potential issue | 🟡 Minor

actionType on Line 172 is assigned but never used.

After switching the analytics call to getActionFromIntent(pnIntent), the local variable actionType (assigned from pnIntent.getStringExtra(REDIRECT_ACTION)) is no longer referenced anywhere in this method. It should be removed to avoid confusion.

🧹 Proposed fix
         Intent pnIntent = checkIfAnyPNIntentPresent();
         if (pnIntent != null) {
-            String actionType = pnIntent.getStringExtra(REDIRECT_ACTION);
             FirebaseAnalyticsUtil.reportNotificationEvent(
                     AnalyticsParamValue.NOTIFICATION_EVENT_TYPE_CLICK,
                     AnalyticsParamValue.REPORT_NOTIFICATION_CLICK_NOTIFICATION_TRAY,
                     getActionFromIntent(pnIntent),
                     pnIntent.getStringExtra(NOTIFICATION_ID)
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/activities/DispatchActivity.java` around lines 170 -
178, Remove the unused local variable actionType assigned in DispatchActivity
(from checkIfAnyPNIntentPresent() result via
pnIntent.getStringExtra(REDIRECT_ACTION)); it is dead code now that
FirebaseAnalyticsUtil.reportNotificationEvent uses
getActionFromIntent(pnIntent). Delete the declaration/assignment of actionType
in the method containing checkIfAnyPNIntentPresent(), leaving the pnIntent
null-check and the call to
FirebaseAnalyticsUtil.reportNotificationEvent(getActionFromIntent(pnIntent),
...) intact.
app/src/org/commcare/connect/ConnectNavHelper.kt (1)

17-31: ⚠️ Potential issue | 🔴 Critical

listener.connectActivityComplete(success) is now called unconditionally—verify that all callers handle both success and failure cases.

The change is intentional: callers need notification when the unlock flow completes regardless of outcome. However, review found a critical issue:

  • LoginActivity.java (line 544-547): The callback executes setResult(RESULT_OK); finish(); unconditionally, ignoring the success parameter. If unlock fails, the activity finishes with RESULT_OK, which is incorrect.
  • BaseDrawerActivity.kt: Three implementations correctly guard operations on if (success).
  • CommCareSetupActivity.java: Empty callback; minimal risk but should handle the result appropriately.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/connect/ConnectNavHelper.kt` around lines 17 - 31, The
unlockAndGoTo flow now always calls listener.connectActivityComplete(success),
so update all callers to handle both success and failure; specifically modify
LoginActivity's ConnectActivityCompleteListener implementation to check the
success boolean and only call setResult(RESULT_OK); finish(); when success is
true (otherwise set an appropriate failure result like RESULT_CANCELED and
surface an error or keep the activity open), ensure CommCareSetupActivity
implements a non-empty handler for failure, and verify the three
BaseDrawerActivity implementations continue to guard post-unlock actions with if
(success) before proceeding; locate these in the ConnectActivityCompleteListener
usages and adjust their callbacks accordingly.
🧹 Nitpick comments (8)
app/src/org/commcare/android/database/connect/models/ConnectDeliveryDetails.java (1)

10-15: Missing getter for unitUUID breaks the accessor pattern — either add it or remove the field.

The unitUUID field is set in the constructor but has no corresponding getter or setter, unlike all other fields in the class. ConnectDeliveryProgressReportAdapter does not need it, but the inconsistency suggests incomplete refactoring. Either add getUnitUUID() and setUnitUUID() for consistency, or remove unitUUID entirely if it's confirmed unused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/org/commcare/android/database/connect/models/ConnectDeliveryDetails.java`
around lines 10 - 15, The ConnectDeliveryDetails class defines unitUUID in the
constructor but lacks accessors; add a public getUnitUUID() (and a public
setUnitUUID(String)) to restore the accessor pattern in ConnectDeliveryDetails
so it matches the other fields and avoids incomplete refactor issues—if you
confirm unitUUID is truly unused across the codebase (including
ConnectDeliveryProgressReportAdapter), alternatively remove the field and
constructor parameter instead, but prefer adding getUnitUUID()/setUnitUUID() for
consistency.
app/src/org/commcare/android/database/connect/models/ConnectJobAssessmentRecord.java (1)

103-105: Consider adding getJobUUID() for consistency with other record types.

ConnectAppRecord and ConnectJobDeliveryRecord both expose getJobUUID(), but ConnectJobAssessmentRecord only has the setter. While the field is queryable via @MetaField(META_JOB_UUID) for database operations, adding the getter would maintain API symmetry with related record types.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/org/commcare/android/database/connect/models/ConnectJobAssessmentRecord.java`
around lines 103 - 105, Add a public getter getJobUUID() to
ConnectJobAssessmentRecord to match the API of ConnectAppRecord and
ConnectJobDeliveryRecord; implement it to return the existing jobUUID field
(annotated with `@MetaField`(META_JOB_UUID)) so callers can read the value
symmetrically with setJobUUID(String jobUUID).
app/src/org/commcare/models/database/connect/ConnectDatabaseUpgrader.java (1)

934-961: Method name upgradePushNotificationRecordToV22 is misleading — it produces V23 records.

This method migrates PushNotificationRecordV21PushNotificationRecordV23 (there is no V22 for this model). The name suggests the target is V22, which is confusing given the later upgradeTwentyTwoTwentyThree and upgradeTwentyThreeTwentyFour steps. Consider renaming to upgradePushNotificationRecordToV23 for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/models/database/connect/ConnectDatabaseUpgrader.java`
around lines 934 - 961, Rename the misleading method
upgradePushNotificationRecordToV22 in ConnectDatabaseUpgrader to
upgradePushNotificationRecordToV23 and update any internal references/callers
accordingly; ensure the method signature and its usage still match the class
(keep logic that adds META_PAYMENT_UUID/META_OPPORTUNITY_UUID columns and the
migration loop converting PushNotificationRecordV21 to PushNotificationRecordV23
via PushNotificationRecordV23.Companion.fromV21, preserving setID and
newStorage.write calls).
app/src/org/commcare/activities/connect/ConnectActivity.java (1)

164-164: Misleading variable name: paymentId holds a UUID.

The variable reads from PAYMENT_UUID but is named paymentId. This is confusing given the PR's purpose of migrating from IDs to UUIDs.

Proposed rename
-            String paymentId = getIntent().getStringExtra(PAYMENT_UUID);
-            if (!TextUtils.isEmpty(paymentId) && job!=null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) {
+            String paymentUuid = getIntent().getStringExtra(PAYMENT_UUID);
+            if (!TextUtils.isEmpty(paymentUuid) && job!=null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/activities/connect/ConnectActivity.java` at line 164,
The variable name paymentId in ConnectActivity (where it reads
getIntent().getStringExtra(PAYMENT_UUID)) is misleading because it actually
contains a UUID; rename paymentId to paymentUuid (or paymentUUID) across the
ConnectActivity class and update all local references (uses, comparisons, logs,
and method parameters in ConnectActivity) to the new name to reflect that it
holds a UUID rather than an integer ID; ensure any related methods or intent
builders that reference the variable name are updated accordingly so compilation
and behavior remain correct.
app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java (1)

8-13: Remove unused imports: CommCareApplication, ConnectDatabaseHelper, SqlStorage, Vector.

These imports are not referenced anywhere in this file and should be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java`
around lines 8 - 13, The file ConnectJobRecord.java contains unused
imports—remove the unused symbols CommCareApplication, ConnectDatabaseHelper,
SqlStorage, and Vector from the import list; keep only imports actually
referenced (for example Persisted, R, Persisting) so the class ConnectJobRecord
compiles without unused-import warnings and the import block is minimal.
app/src/org/commcare/connect/database/JobStoreManager.java (1)

149-259: Migration runs outside the main storeJobs transaction — confirm idempotency guarantees.

migrateOtherModelsForJobUUID commits its own transaction (line 255) before storeJobs opens the main transaction at line 42. If the main transaction fails and rolls back, the UUID migration will have already been committed. This is acceptable because the migration is idempotent (line 157 short-circuits when the UUID is already up-to-date), but be aware that a partial failure leaves UUIDs updated without the new job data being stored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/connect/database/JobStoreManager.java` around lines 149
- 259, migrateOtherModelsForJobUUID currently starts/commits its own DB
transaction
(ConnectDatabaseHelper.connectDatabase.beginTransaction()/setTransactionSuccessful()/endTransaction())
which can commit UUID changes even if the caller storeJobs later rolls back;
remove the internal transaction from migrateOtherModelsForJobUUID (or add a
boolean parameter like useInternalTransaction) and instead perform its writes
under the caller's transaction (storeJobs) so UUID updates are atomic with job
storage, or explicitly call it only inside an already-open transaction; update
storeJobs to call migrateOtherModelsForJobUUID inside its transaction (or pass
useInternalTransaction=true when needed) and remove the standalone
beginTransaction()/setTransactionSuccessful()/endTransaction() calls from
migrateOtherModelsForJobUUID.
app/src/org/commcare/utils/FirebaseMessagingUtil.java (1)

543-551: Inconsistent null/empty check between getActionFromPayload and getActionFromIntent.

getActionFromPayload uses payload.containsKey(NOTIFICATION_KEY) (allows empty-string values), while getActionFromIntent uses !TextUtils.isEmpty(intent.getStringExtra(NOTIFICATION_KEY)) (rejects empty strings). If the server sends key: "", the payload version returns "" while the intent version falls back to REDIRECT_ACTION. Consider aligning the two.

Proposed alignment
 public static String getActionFromPayload(Map<String, String> payload) {
-    return (CCC_GENERIC_OPPORTUNITY.equals(payload.get(REDIRECT_ACTION)) &&
-            payload.containsKey(NOTIFICATION_KEY)) ? payload.get(NOTIFICATION_KEY) : payload.get(REDIRECT_ACTION);
+    return (CCC_GENERIC_OPPORTUNITY.equals(payload.get(REDIRECT_ACTION)) &&
+            !TextUtils.isEmpty(payload.get(NOTIFICATION_KEY))) ? payload.get(NOTIFICATION_KEY) : payload.get(REDIRECT_ACTION);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/utils/FirebaseMessagingUtil.java` around lines 543 -
551, The two helpers getActionFromPayload and getActionFromIntent are
inconsistent: getActionFromPayload uses payload.containsKey(NOTIFICATION_KEY)
which allows empty strings while getActionFromIntent rejects empty values with
TextUtils.isEmpty; change getActionFromPayload to the same non-empty check by
replacing payload.containsKey(NOTIFICATION_KEY) with
!TextUtils.isEmpty(payload.get(NOTIFICATION_KEY)) so both methods treat empty
notification keys the same and safely fall back to REDIRECT_ACTION.
app/src/org/commcare/pn/workers/NotificationsSyncWorker.kt (1)

188-211: Consider using a named argument for clarity.

The third argument to ConnectJobHelper.updateJobProgress is the showLoading parameter (Boolean, default null). In a background worker context, passing false is semantically appropriate. However, using a named argument would improve readability: showLoading = false instead of the positional false.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/pn/workers/NotificationsSyncWorker.kt` around lines 188
- 211, In syncLearnOrDeliveryProgress(), the call to
ConnectJobHelper.updateJobProgress passes the Boolean positional argument for
showLoading (currently "false"), which reduces clarity; update the call to use a
named argument (showLoading = false) when invoking
ConnectJobHelper.updateJobProgress inside the suspendCoroutine block (the method
using ConnectActivityCompleteListener), so the intent is explicit while leaving
other arguments unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/org/commcare/android/database/connect/models/ConnectLearnModuleSummaryRecord.java`:
- Around line 114-117: The class ConnectLearnModuleSummaryRecord has a public
setter setJobUUID(String) but lacks the corresponding getter; add a public
String getJobUUID() method to ConnectLearnModuleSummaryRecord that returns the
jobUUID field so the class exposes a matching public getter/setter pair
consistent with ConnectAppRecord and ConnectJobDeliveryRecord.

In
`@app/src/org/commcare/android/database/connect/models/ConnectPaymentUnitRecord.java`:
- Around line 130-132: Add a missing getter for the persisted jobUUID field in
ConnectPaymentUnitRecord to pair with the existing setJobUUID(String) method:
implement a public String getJobUUID() that returns jobUUID so callers can read
the value; update the class ConnectPaymentUnitRecord only (no removal or changes
to fromJson()/fromV21()) to maintain symmetry with setJobUUID and match other
classes like ConnectJobRecord/ConnectJobDeliveryRecord/ConnectAppRecord.

In `@app/src/org/commcare/pn/workermanager/NotificationsSyncWorkerManager.kt`:
- Around line 268-277: The method startLearnOrDeliverySyncWorker currently only
checks notificationPayload.containsKey(OPPORTUNITY_UUID) and can proceed with a
null/empty opportunityUUID; update it to mirror
startLearningSyncWorker/startDeliverySyncWorker by retrieving val
opportunityUUID = notificationPayload[OPPORTUNITY_UUID] and verifying
!TextUtils.isEmpty(opportunityUUID) (or opportunityUUID.isNullOrEmpty()
inverted) before calling startWorkRequest with
SyncAction.SYNC_GENERIC_OPPORTUNITY so you never build a malformed unique name;
keep references to startWorkRequest and SyncAction.SYNC_GENERIC_OPPORTUNITY
unchanged.

In `@app/src/org/commcare/utils/FirebaseMessagingUtil.java`:
- Around line 277-283: handleGenericPushNotification currently builds an intent
and shows a notification without validating that the payload contains
OPPORTUNITY_UUID; replicate the guard used in
handleResumeLearningOrDeliveryJobPushNotification and
handleOpportunitySummaryPagePushNotification by checking
fcmMessageData.getData().containsKey(OPPORTUNITY_UUID) (or equivalent accessor)
before calling
getConnectActivityNotification/buildNotification/showNotification, log an error
and return null when the UUID is missing (so background sync/handleNoConnectJob
won't break), and keep getConnectActivityNotification, buildNotification and
showNotification usage otherwise.
- Around line 513-531: The empty catch in getIntentForPNIfAny silently swallows
all exceptions; change it to catch Exception e and log the exception at debug
(or appropriate) level so programming errors like NPE/ClassCast are visible
while preserving current flow—use the existing logger (or add one) to log a
short message and the exception when handling intent extras and calling
handleNotification (refer to getIntentForPNIfAny, intent.replaceExtras, and
handleNotification) and then continue to return null as before.

In
`@app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java`:
- Around line 417-419: Add a per-version comment for the three new
externalizable entries and move PushNotificationRecordV21 into the existing 2.62
block: update the list so
"org.commcare.android.database.connect.models.PushNotificationRecordV21" is
grouped with the other V21 variants under the "// Added in 2.62" comment, and
add a new comment (e.g., "// Added in 2.63" or the correct release tag)
immediately above the two ConnectJobDeliveryRecordV22 and
PushNotificationRecordV23 entries to match the file's per-version sectioning
convention.

---

Outside diff comments:
In `@app/src/org/commcare/activities/DispatchActivity.java`:
- Around line 170-178: Remove the unused local variable actionType assigned in
DispatchActivity (from checkIfAnyPNIntentPresent() result via
pnIntent.getStringExtra(REDIRECT_ACTION)); it is dead code now that
FirebaseAnalyticsUtil.reportNotificationEvent uses
getActionFromIntent(pnIntent). Delete the declaration/assignment of actionType
in the method containing checkIfAnyPNIntentPresent(), leaving the pnIntent
null-check and the call to
FirebaseAnalyticsUtil.reportNotificationEvent(getActionFromIntent(pnIntent),
...) intact.

In `@app/src/org/commcare/connect/ConnectNavHelper.kt`:
- Around line 17-31: The unlockAndGoTo flow now always calls
listener.connectActivityComplete(success), so update all callers to handle both
success and failure; specifically modify LoginActivity's
ConnectActivityCompleteListener implementation to check the success boolean and
only call setResult(RESULT_OK); finish(); when success is true (otherwise set an
appropriate failure result like RESULT_CANCELED and surface an error or keep the
activity open), ensure CommCareSetupActivity implements a non-empty handler for
failure, and verify the three BaseDrawerActivity implementations continue to
guard post-unlock actions with if (success) before proceeding; locate these in
the ConnectActivityCompleteListener usages and adjust their callbacks
accordingly.

---

Nitpick comments:
In `@app/src/org/commcare/activities/connect/ConnectActivity.java`:
- Line 164: The variable name paymentId in ConnectActivity (where it reads
getIntent().getStringExtra(PAYMENT_UUID)) is misleading because it actually
contains a UUID; rename paymentId to paymentUuid (or paymentUUID) across the
ConnectActivity class and update all local references (uses, comparisons, logs,
and method parameters in ConnectActivity) to the new name to reflect that it
holds a UUID rather than an integer ID; ensure any related methods or intent
builders that reference the variable name are updated accordingly so compilation
and behavior remain correct.

In
`@app/src/org/commcare/android/database/connect/models/ConnectDeliveryDetails.java`:
- Around line 10-15: The ConnectDeliveryDetails class defines unitUUID in the
constructor but lacks accessors; add a public getUnitUUID() (and a public
setUnitUUID(String)) to restore the accessor pattern in ConnectDeliveryDetails
so it matches the other fields and avoids incomplete refactor issues—if you
confirm unitUUID is truly unused across the codebase (including
ConnectDeliveryProgressReportAdapter), alternatively remove the field and
constructor parameter instead, but prefer adding getUnitUUID()/setUnitUUID() for
consistency.

In
`@app/src/org/commcare/android/database/connect/models/ConnectJobAssessmentRecord.java`:
- Around line 103-105: Add a public getter getJobUUID() to
ConnectJobAssessmentRecord to match the API of ConnectAppRecord and
ConnectJobDeliveryRecord; implement it to return the existing jobUUID field
(annotated with `@MetaField`(META_JOB_UUID)) so callers can read the value
symmetrically with setJobUUID(String jobUUID).

In `@app/src/org/commcare/android/database/connect/models/ConnectJobRecord.java`:
- Around line 8-13: The file ConnectJobRecord.java contains unused
imports—remove the unused symbols CommCareApplication, ConnectDatabaseHelper,
SqlStorage, and Vector from the import list; keep only imports actually
referenced (for example Persisted, R, Persisting) so the class ConnectJobRecord
compiles without unused-import warnings and the import block is minimal.

In `@app/src/org/commcare/connect/database/JobStoreManager.java`:
- Around line 149-259: migrateOtherModelsForJobUUID currently starts/commits its
own DB transaction
(ConnectDatabaseHelper.connectDatabase.beginTransaction()/setTransactionSuccessful()/endTransaction())
which can commit UUID changes even if the caller storeJobs later rolls back;
remove the internal transaction from migrateOtherModelsForJobUUID (or add a
boolean parameter like useInternalTransaction) and instead perform its writes
under the caller's transaction (storeJobs) so UUID updates are atomic with job
storage, or explicitly call it only inside an already-open transaction; update
storeJobs to call migrateOtherModelsForJobUUID inside its transaction (or pass
useInternalTransaction=true when needed) and remove the standalone
beginTransaction()/setTransactionSuccessful()/endTransaction() calls from
migrateOtherModelsForJobUUID.

In `@app/src/org/commcare/models/database/connect/ConnectDatabaseUpgrader.java`:
- Around line 934-961: Rename the misleading method
upgradePushNotificationRecordToV22 in ConnectDatabaseUpgrader to
upgradePushNotificationRecordToV23 and update any internal references/callers
accordingly; ensure the method signature and its usage still match the class
(keep logic that adds META_PAYMENT_UUID/META_OPPORTUNITY_UUID columns and the
migration loop converting PushNotificationRecordV21 to PushNotificationRecordV23
via PushNotificationRecordV23.Companion.fromV21, preserving setID and
newStorage.write calls).

In `@app/src/org/commcare/pn/workers/NotificationsSyncWorker.kt`:
- Around line 188-211: In syncLearnOrDeliveryProgress(), the call to
ConnectJobHelper.updateJobProgress passes the Boolean positional argument for
showLoading (currently "false"), which reduces clarity; update the call to use a
named argument (showLoading = false) when invoking
ConnectJobHelper.updateJobProgress inside the suspendCoroutine block (the method
using ConnectActivityCompleteListener), so the intent is explicit while leaving
other arguments unchanged.

In `@app/src/org/commcare/utils/FirebaseMessagingUtil.java`:
- Around line 543-551: The two helpers getActionFromPayload and
getActionFromIntent are inconsistent: getActionFromPayload uses
payload.containsKey(NOTIFICATION_KEY) which allows empty strings while
getActionFromIntent rejects empty values with TextUtils.isEmpty; change
getActionFromPayload to the same non-empty check by replacing
payload.containsKey(NOTIFICATION_KEY) with
!TextUtils.isEmpty(payload.get(NOTIFICATION_KEY)) so both methods treat empty
notification keys the same and safely fall back to REDIRECT_ACTION.

Comment on lines +114 to +117

public void setJobUUID(String jobUUID) {
this.jobUUID = jobUUID;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the ConnectLearnModuleSummaryRecord file
find . -name "ConnectLearnModuleSummaryRecord.java" -type f

Repository: dimagi/commcare-android

Length of output: 158


🏁 Script executed:

# Let's also find other Connect record files for comparison
find . -name "ConnectAppRecord.java" -o -name "ConnectDeliveryRecord.java" -type f

Repository: dimagi/commcare-android

Length of output: 143


🏁 Script executed:

# Search for all jobUUID getter methods in the codebase to see the pattern
rg "getJobUUID" --type java -B 2 -A 2

Repository: dimagi/commcare-android

Length of output: 19650


🏁 Script executed:

# Check for usage of getJobUUID() calls to see if they're actually needed
rg "\.getJobUUID\(\)" --type java -B 2 -A 2

Repository: dimagi/commcare-android

Length of output: 18266


🏁 Script executed:

# Read the ConnectLearnModuleSummaryRecord file to verify the current state
cat -n ./app/src/org/commcare/android/database/connect/models/ConnectLearnModuleSummaryRecord.java | head -150

Repository: dimagi/commcare-android

Length of output: 4791


🏁 Script executed:

# Also check if there are any getJobUUID() method definitions in ConnectLearnModuleSummaryRecord
rg "public String getJobUUID|public void setJobUUID" app/src/org/commcare/android/database/connect/models/ConnectLearnModuleSummaryRecord.java

Repository: dimagi/commcare-android

Length of output: 111


Add missing getJobUUID() getter to match setter.

The setJobUUID() method exists but the corresponding getter is missing. Other similar record classes in the same module (e.g., ConnectAppRecord, ConnectJobDeliveryRecord) expose both setter and getter for jobUUID, ensuring consistent API patterns. Add the getter to maintain parity:

Proposed addition
    public void setJobUUID(String jobUUID) {
        this.jobUUID = jobUUID;
    }
+
+    public String getJobUUID() {
+        return jobUUID;
+    }
}
📝 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 setJobUUID(String jobUUID) {
this.jobUUID = jobUUID;
}
public void setJobUUID(String jobUUID) {
this.jobUUID = jobUUID;
}
public String getJobUUID() {
return jobUUID;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/org/commcare/android/database/connect/models/ConnectLearnModuleSummaryRecord.java`
around lines 114 - 117, The class ConnectLearnModuleSummaryRecord has a public
setter setJobUUID(String) but lacks the corresponding getter; add a public
String getJobUUID() method to ConnectLearnModuleSummaryRecord that returns the
jobUUID field so the class exposes a matching public getter/setter pair
consistent with ConnectAppRecord and ConnectJobDeliveryRecord.

Comment on lines +130 to +132
public void setJobUUID(String jobUUID) {
this.jobUUID = jobUUID;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for all callers of getUnitId() in the codebase
echo "=== All callers of getUnitId() ==="
rg -n 'getUnitId\(\)' --type java

echo ""
echo "=== Callers of getJobUUID() ==="
rg -n 'getJobUUID\(\)' --type java

echo ""
echo "=== ConnectPaymentUnitRecord file content (full) ==="
find . -name "ConnectPaymentUnitRecord.java" -type f | head -1 | xargs cat -n

Repository: dimagi/commcare-android

Length of output: 11081


Add missing getJobUUID() getter to complete the UUID migration pattern.

The setJobUUID() setter (lines 130–132) should have a paired getter. Currently:

  • jobUUID field is persisted, written via fromJson() and fromV21(), and exposed for mutation via setJobUUID() — but cannot be read back via getter.
  • Other Connect record classes (ConnectJobRecord, ConnectJobDeliveryRecord, ConnectAppRecord) all expose getJobUUID() for consistency.
  • The unitUUID getter exists (lines 134–136) with no setter, reinforcing the asymmetry.

Regarding getUnitId() removal: No remaining callers of getUnitId() on ConnectPaymentUnitRecord were found. The removal is safe; the field migration in fromV21() (lines 93–94) correctly calls getUnitId() on the legacy ConnectPaymentUnitRecordV21 record, not on the new class.

Add getJobUUID() to align with the UUID identity pattern used throughout the Connect module:

public String getJobUUID() {
    return jobUUID;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/org/commcare/android/database/connect/models/ConnectPaymentUnitRecord.java`
around lines 130 - 132, Add a missing getter for the persisted jobUUID field in
ConnectPaymentUnitRecord to pair with the existing setJobUUID(String) method:
implement a public String getJobUUID() that returns jobUUID so callers can read
the value; update the class ConnectPaymentUnitRecord only (no removal or changes
to fromJson()/fromV21()) to maintain symmetry with setJobUUID and match other
classes like ConnectJobRecord/ConnectJobDeliveryRecord/ConnectAppRecord.

Comment on lines +268 to +277
private fun startLearnOrDeliverySyncWorker(notificationPayload: Map<String, String>) {
if (notificationPayload.containsKey(OPPORTUNITY_UUID) && cccCheckPassed(context)) {
val opportunityUUID = notificationPayload.get(OPPORTUNITY_UUID)
startWorkRequest(
notificationPayload,
SyncAction.SYNC_GENERIC_OPPORTUNITY,
SyncAction.SYNC_GENERIC_OPPORTUNITY.toString() + "-$opportunityUUID",
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent UUID validation compared to sibling methods.

startLearningSyncWorker and startDeliverySyncWorker both validate with !TextUtils.isEmpty(opportunityUUID), but startLearnOrDeliverySyncWorker only checks containsKey(OPPORTUNITY_UUID). If the key is present with an empty or null value, this method will proceed and enqueue a work request with a malformed unique name (e.g., "SYNC_GENERIC_OPPORTUNITY-" or "SYNC_GENERIC_OPPORTUNITY-null").

Proposed fix
     private fun startLearnOrDeliverySyncWorker(notificationPayload: Map<String, String>) {
-        if (notificationPayload.containsKey(OPPORTUNITY_UUID) && cccCheckPassed(context)) {
-            val opportunityUUID = notificationPayload.get(OPPORTUNITY_UUID)
+        val opportunityUUID = notificationPayload.get(OPPORTUNITY_UUID)
+        if (!TextUtils.isEmpty(opportunityUUID) && cccCheckPassed(context)) {
             startWorkRequest(
                 notificationPayload,
                 SyncAction.SYNC_GENERIC_OPPORTUNITY,
📝 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 startLearnOrDeliverySyncWorker(notificationPayload: Map<String, String>) {
if (notificationPayload.containsKey(OPPORTUNITY_UUID) && cccCheckPassed(context)) {
val opportunityUUID = notificationPayload.get(OPPORTUNITY_UUID)
startWorkRequest(
notificationPayload,
SyncAction.SYNC_GENERIC_OPPORTUNITY,
SyncAction.SYNC_GENERIC_OPPORTUNITY.toString() + "-$opportunityUUID",
)
}
}
private fun startLearnOrDeliverySyncWorker(notificationPayload: Map<String, String>) {
val opportunityUUID = notificationPayload.get(OPPORTUNITY_UUID)
if (!TextUtils.isEmpty(opportunityUUID) && cccCheckPassed(context)) {
startWorkRequest(
notificationPayload,
SyncAction.SYNC_GENERIC_OPPORTUNITY,
SyncAction.SYNC_GENERIC_OPPORTUNITY.toString() + "-$opportunityUUID",
)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/pn/workermanager/NotificationsSyncWorkerManager.kt`
around lines 268 - 277, The method startLearnOrDeliverySyncWorker currently only
checks notificationPayload.containsKey(OPPORTUNITY_UUID) and can proceed with a
null/empty opportunityUUID; update it to mirror
startLearningSyncWorker/startDeliverySyncWorker by retrieving val
opportunityUUID = notificationPayload[OPPORTUNITY_UUID] and verifying
!TextUtils.isEmpty(opportunityUUID) (or opportunityUUID.isNullOrEmpty()
inverted) before calling startWorkRequest with
SyncAction.SYNC_GENERIC_OPPORTUNITY so you never build a malformed unique name;
keep references to startWorkRequest and SyncAction.SYNC_GENERIC_OPPORTUNITY
unchanged.

Comment on lines +277 to +283
private static Intent handleGenericPushNotification(Context context, FCMMessageData fcmMessageData, boolean showNotification) {
Intent intent = getConnectActivityNotification(context, fcmMessageData);
if (showNotification)
showNotification(context, buildNotification(context, intent, fcmMessageData),
fcmMessageData);
return intent;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

handleGenericPushNotification doesn't validate OPPORTUNITY_UUID presence, unlike peer handlers.

Other handlers (handleResumeLearningOrDeliveryJobPushNotification at line 265, handleOpportunitySummaryPagePushNotification at line 294) check for OPPORTUNITY_UUID and return null with an error log when absent. This handler unconditionally creates an intent and shows a notification. If the server sends a malformed payload without the UUID, the user sees a notification, but the background sync will fail (handleNoConnectJob()), and tapping the notification may lead to a broken screen.

Consider adding the same validation guard present in peer handlers.

Proposed fix
 private static Intent handleGenericPushNotification(Context context, FCMMessageData fcmMessageData, boolean showNotification) {
+    if (!fcmMessageData.getPayloadData().containsKey(OPPORTUNITY_UUID)) {
+        Logger.exception("Empty push notification for action 'ccc_generic_opportunity'",
+                new Throwable("Empty notification without 'opportunity' details"));
+        return null;
+    }
     Intent intent = getConnectActivityNotification(context, fcmMessageData);
     if (showNotification)
         showNotification(context, buildNotification(context, intent, fcmMessageData),
                 fcmMessageData);
     return intent;
 }
📝 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 static Intent handleGenericPushNotification(Context context, FCMMessageData fcmMessageData, boolean showNotification) {
Intent intent = getConnectActivityNotification(context, fcmMessageData);
if (showNotification)
showNotification(context, buildNotification(context, intent, fcmMessageData),
fcmMessageData);
return intent;
}
private static Intent handleGenericPushNotification(Context context, FCMMessageData fcmMessageData, boolean showNotification) {
if (!fcmMessageData.getPayloadData().containsKey(OPPORTUNITY_UUID)) {
Logger.exception("Empty push notification for action 'ccc_generic_opportunity'",
new Throwable("Empty notification without 'opportunity' details"));
return null;
}
Intent intent = getConnectActivityNotification(context, fcmMessageData);
if (showNotification)
showNotification(context, buildNotification(context, intent, fcmMessageData),
fcmMessageData);
return intent;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/utils/FirebaseMessagingUtil.java` around lines 277 -
283, handleGenericPushNotification currently builds an intent and shows a
notification without validating that the payload contains OPPORTUNITY_UUID;
replicate the guard used in handleResumeLearningOrDeliveryJobPushNotification
and handleOpportunitySummaryPagePushNotification by checking
fcmMessageData.getData().containsKey(OPPORTUNITY_UUID) (or equivalent accessor)
before calling
getConnectActivityNotification/buildNotification/showNotification, log an error
and return null when the UUID is missing (so background sync/handleNoConnectJob
won't break), and keep getConnectActivityNotification, buildNotification and
showNotification usage otherwise.

Comment on lines +513 to 531
public static Intent getIntentForPNIfAny(Context context, Intent intent) {
// Added try-catch to avoid any data corruption handling, and dispatch activity flow is not disturbed.
try {
if (intent != null && intent.getExtras() != null) {
Map<String, String> dataPayload = new HashMap<>();
for (String key : intent.getExtras().keySet()) {
String value = intent.getExtras().getString(key);
dataPayload.put(key, value);
}
Intent pnIntent = handleNotification(context, dataPayload, null, false);
if (pnIntent != null) {
intent.replaceExtras(new Bundle()); // clear intents if push notification intents are present else app keeps reloading same PN intents
}
return pnIntent;
}
return pnIntent;
} catch (Exception e) { // no need to log
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty catch block silently swallows all exceptions, including programming errors.

The catch at line 528 discards exceptions with no logging. While the intent is to prevent dispatch flow disruption, silently swallowing NullPointerException, ClassCastException, etc. makes bugs nearly impossible to diagnose in production. At minimum, add a debug-level log.

Proposed fix
-        } catch (Exception e) { // no need to log
+        } catch (Exception e) {
+            Logger.log(LogTypes.TYPE_FCM, "Error processing PN intent: " + e.getMessage());
         }
📝 Committable suggestion

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

Suggested change
public static Intent getIntentForPNIfAny(Context context, Intent intent) {
// Added try-catch to avoid any data corruption handling, and dispatch activity flow is not disturbed.
try {
if (intent != null && intent.getExtras() != null) {
Map<String, String> dataPayload = new HashMap<>();
for (String key : intent.getExtras().keySet()) {
String value = intent.getExtras().getString(key);
dataPayload.put(key, value);
}
Intent pnIntent = handleNotification(context, dataPayload, null, false);
if (pnIntent != null) {
intent.replaceExtras(new Bundle()); // clear intents if push notification intents are present else app keeps reloading same PN intents
}
return pnIntent;
}
return pnIntent;
} catch (Exception e) { // no need to log
}
return null;
}
public static Intent getIntentForPNIfAny(Context context, Intent intent) {
// Added try-catch to avoid any data corruption handling, and dispatch activity flow is not disturbed.
try {
if (intent != null && intent.getExtras() != null) {
Map<String, String> dataPayload = new HashMap<>();
for (String key : intent.getExtras().keySet()) {
String value = intent.getExtras().getString(key);
dataPayload.put(key, value);
}
Intent pnIntent = handleNotification(context, dataPayload, null, false);
if (pnIntent != null) {
intent.replaceExtras(new Bundle()); // clear intents if push notification intents are present else app keeps reloading same PN intents
}
return pnIntent;
}
} catch (Exception e) {
Logger.log(LogTypes.TYPE_FCM, "Error processing PN intent: " + e.getMessage());
}
return null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/utils/FirebaseMessagingUtil.java` around lines 513 -
531, The empty catch in getIntentForPNIfAny silently swallows all exceptions;
change it to catch Exception e and log the exception at debug (or appropriate)
level so programming errors like NPE/ClassCast are visible while preserving
current flow—use the existing logger (or add one) to log a short message and the
exception when handling intent extras and calling handleNotification (refer to
getIntentForPNIfAny, intent.replaceExtras, and handleNotification) and then
continue to return null as before.

Comment on lines +417 to +419
"org.commcare.android.database.connect.models.PushNotificationRecordV21",
"org.commcare.android.database.connect.models.ConnectJobDeliveryRecordV22",
"org.commcare.android.database.connect.models.PushNotificationRecordV23"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

New externalizable entries are missing a version comment.

The three new entries are appended after the // Added in 2.62 block without a version label. The rest of the list uses per-version section comments. Additionally, PushNotificationRecordV21 belongs logically with the other V21 variants already in the 2.62 block — it appears it may have been missed in the prior PR.

✏️ Suggested format
+           // Added in 2.62 (missed)
+           "org.commcare.android.database.connect.models.PushNotificationRecordV21",
+
+           // Added in 2.XX
+           "org.commcare.android.database.connect.models.ConnectJobDeliveryRecordV22",
+           "org.commcare.android.database.connect.models.PushNotificationRecordV23"
-           "org.commcare.android.database.connect.models.PushNotificationRecordV21",
-           "org.commcare.android.database.connect.models.ConnectJobDeliveryRecordV22",
-           "org.commcare.android.database.connect.models.PushNotificationRecordV23"
📝 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
"org.commcare.android.database.connect.models.PushNotificationRecordV21",
"org.commcare.android.database.connect.models.ConnectJobDeliveryRecordV22",
"org.commcare.android.database.connect.models.PushNotificationRecordV23"
// Added in 2.62 (missed)
"org.commcare.android.database.connect.models.PushNotificationRecordV21",
// Added in 2.XX
"org.commcare.android.database.connect.models.ConnectJobDeliveryRecordV22",
"org.commcare.android.database.connect.models.PushNotificationRecordV23"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/unit-tests/src/org/commcare/android/tests/processing/FormStorageTest.java`
around lines 417 - 419, Add a per-version comment for the three new
externalizable entries and move PushNotificationRecordV21 into the existing 2.62
block: update the list so
"org.commcare.android.database.connect.models.PushNotificationRecordV21" is
grouped with the other V21 variants under the "// Added in 2.62" comment, and
add a new comment (e.g., "// Added in 2.63" or the correct release tag)
immediately above the two ConnectJobDeliveryRecordV22 and
PushNotificationRecordV23 entries to match the file's per-version sectioning
convention.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-integration-tests Skip android tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments