Skip to content

Conversation

@jaypanchal-13
Copy link
Contributor

Product Description

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

Technical Summary

  • Created and added notification read/unread status in shared pref
  • Added broadcast receiver for notification status

Feature Flag

Safety Assurance

Safety story

Automated test coverage

QA Plan

Labels and Review

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

@jaypanchal-13 jaypanchal-13 changed the title Managed read/unread pn icon status and its icon Managed read/unread pn status and its icon Oct 27, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

📝 Walkthrough

Walkthrough

This PR implements notification read/unread status tracking for push notifications. It introduces a new NotificationPrefs singleton to manage read status via SharedPreferences, updates PushNotificationApiHelper to conditionally broadcast and set notification status based on request origin, marks notifications as read when navigated to via ConnectNavHelper, adds a broadcast receiver in BaseDrawerActivity to listen for notification updates, and modifies BaseDrawerController to display different notification icons (bell vs. new notification bell) based on read status. The PushNotificationViewModel now passes an isFromPNActivity flag to distinguish between background refreshes and user-initiated requests from the notification activity.

Sequence Diagram

sequenceDiagram
    actor User
    participant DrawerUI as Drawer UI
    participant NavHelper as ConnectNavHelper
    participant PNActivity as PushNotificationActivity
    participant ViewModel as PushNotificationViewModel
    participant APIHelper as PushNotificationApiHelper
    participant Prefs as NotificationPrefs
    participant Broadcaster as LocalBroadcastManager
    participant DrawerActivity as BaseDrawerActivity

    User->>DrawerUI: Receives unread notification
    Note over DrawerUI: Icon shows ic_new_notification_bell
    
    User->>DrawerUI: Taps notification
    DrawerUI->>NavHelper: goToNotification()
    NavHelper->>Prefs: setNotificationAsRead(context)
    NavHelper->>PNActivity: Launch PushNotificationActivity
    
    PNActivity->>ViewModel: Refresh notifications
    ViewModel->>APIHelper: retrieveLatestPushNotifications(context, true)
    rect rgb(200, 230, 255)
        Note over APIHelper: isFromPNActivity=true
        APIHelper->>APIHelper: Fetch notifications
        APIHelper-->>ViewModel: Return results (no side effects)
    end
    
    rect rgb(230, 200, 255)
        Note over APIHelper: Background refresh or ViewModel refresh
        APIHelper->>Prefs: setNotificationAsUnread(context)
        APIHelper->>Broadcaster: Broadcast ACTION_NEW_NOTIFICATIONS
    end
    
    Broadcaster->>DrawerActivity: notificationUpdateReceiver receives broadcast
    DrawerActivity->>DrawerActivity: refreshDrawer()
    
    DrawerUI->>Prefs: getNotificationReadStatus()
    Note over DrawerUI: Icon shows ic_bell (read status)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • NotificationPrefs.kt: New singleton with straightforward SharedPreferences wrapper—low complexity but requires validation of correct key usage across codebase.
  • PushNotificationApiHelper.kt: Introduces conditional logic based on isFromPNActivity flag with side-effect branching; verify that broadcast and preference updates are correctly gated.
  • BaseDrawerActivity.kt: Broadcast receiver registration/unregistration logic—verify lifecycle correctness and potential edge cases with overlapping receiver registration.
  • BaseDrawerController.kt: Icon selection logic change—ensure both drawable resources exist and the read-status preference is consistently maintained.

Possibly related PRs

Suggested labels

skip-integration-tests

Suggested reviewers

  • OrangeAndGreen
  • shubham1g5
  • Jignesh-dimagi

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description is largely incomplete against the required template. While it provides the ticket link and minimal technical bullets under "Technical Summary," it is missing critical sections: the Product Description section is not filled out, the Feature Flag section is empty, and the entire Safety Assurance section—including Safety story, Automated test coverage, and QA Plan—is left blank. Additionally, the Labels and Review checklist is present but the items remain unchecked and unaddressed, suggesting the author did not complete the quality assurance and release planning steps. Only the Technical Summary is partially addressed with a ticket link and two high-level bullet points, but without rationale or design decision details.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Managed read/unread pn status and its icon" directly aligns with the primary changes in this pull request. The changeset introduces a new NotificationPrefs object to manage read/unread status, modifies the notification icon logic in BaseDrawerController to display different icons based on read status, and adds supporting infrastructure including broadcast receivers and API flag plumbing. The title accurately summarizes the core functionality being implemented—managing notification read/unread state and its corresponding visual representation—and is sufficiently specific that a teammate scanning commit history would understand the main change.
✨ 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 CCCT-1839-pm-read-unread-status

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
app/src/org/commcare/navdrawer/BaseDrawerActivity.kt (1)

23-27: Consider consolidating receiver logic.

The notificationUpdateReceiver and messagingUpdateReceiver (lines 55-59) both perform the same action: refreshing the drawer. While keeping them separate provides clarity about their distinct triggers, consolidating them into a single receiver handling multiple intent filters could reduce duplication.

Example:

-private val notificationUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
-    override fun onReceive(context: Context, intent: Intent) {
-        refreshDrawer()
-    }
-}
-
-private val messagingUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
-    override fun onReceive(context: Context, intent: Intent) {
-        drawerController?.refreshDrawerContent()
-    }
-}
+private val updateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+    override fun onReceive(context: Context, intent: Intent) {
+        refreshDrawer()
+    }
+}

Then register once with both filters.

app/src/org/commcare/utils/PushNotificationApiHelper.kt (1)

45-51: Clarify the dual update handling pattern.

The conditional if (result.isNotEmpty() && !isFromPNActivity) creates two distinct handling paths:

  1. Background fetch (isFromPNActivity=false): Stores notifications, acknowledges to server, marks as unread, and broadcasts.
  2. PN activity fetch (isFromPNActivity=true): Returns data only; PushNotificationViewModel delegates storage/acknowledgment to PNApiSyncWorkerManager.

This split responsibility across different layers (API helper vs. ViewModel's worker manager) isn't immediately obvious and could complicate maintenance. Consider:

  • Adding a comment explaining why updates are skipped when isFromPNActivity=true.
  • Evaluating whether both code paths could be unified, or if the separation serves a specific performance/UX goal.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6dd813 and 1cc864b.

📒 Files selected for processing (6)
  • app/src/org/commcare/activities/connect/viewmodel/PushNotificationViewModel.kt (1 hunks)
  • app/src/org/commcare/connect/ConnectNavHelper.kt (2 hunks)
  • app/src/org/commcare/navdrawer/BaseDrawerActivity.kt (2 hunks)
  • app/src/org/commcare/navdrawer/BaseDrawerController.kt (2 hunks)
  • app/src/org/commcare/preferences/NotificationPrefs.kt (1 hunks)
  • app/src/org/commcare/utils/PushNotificationApiHelper.kt (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/org/commcare/activities/connect/viewmodel/PushNotificationViewModel.kt (1)
app/src/org/commcare/utils/PushNotificationApiHelper.kt (1)
  • retrieveLatestPushNotifications (32-35)
⏰ 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 (8)
app/src/org/commcare/preferences/NotificationPrefs.kt (2)

10-13: LGTM!

Clean implementation using Kotlin's edit extension for SharedPreferences.


20-23: Verify the default value semantics.

The default value of true means notifications start in a "read" state on fresh installs. This approach prevents showing a notification badge when there are no actual notifications, which seems reasonable. However, ensure this aligns with the intended UX—particularly that the badge only appears after the first actual notification is received and marked unread.

app/src/org/commcare/connect/ConnectNavHelper.kt (1)

42-42: LGTM!

Correctly marks notifications as read before launching the notification activity, ensuring the icon state updates appropriately.

app/src/org/commcare/activities/connect/viewmodel/PushNotificationViewModel.kt (1)

39-39: LGTM!

Correctly passes isFromPNActivity = true to distinguish user-initiated refreshes from background fetches, enabling the API helper to skip redundant broadcasts and status updates.

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

136-139: LGTM!

Clean conditional logic that dynamically selects the notification icon based on read status, integrated appropriately within the drawer refresh flow.

app/src/org/commcare/navdrawer/BaseDrawerActivity.kt (1)

39-42: Note: LocalBroadcastManager is deprecated.

While LocalBroadcastManager is deprecated as of AndroidX, it remains functional. For future work, consider migrating to alternatives like LiveData, SharedFlow, or a custom event bus. For now, this implementation is acceptable and consistent with existing patterns in the codebase.

Also applies to: 48-48

app/src/org/commcare/utils/PushNotificationApiHelper.kt (2)

30-30: LGTM!

Clear constant definition for the notification broadcast action.


49-50: Note: LocalBroadcastManager is deprecated.

LocalBroadcastManager is deprecated as of AndroidX. While it remains functional, consider migrating to alternatives (e.g., LiveData, SharedFlow, or a custom event bus) in future iterations. For now, this is acceptable given its usage throughout the codebase.

Comment on lines 39 to 42
LocalBroadcastManager.getInstance(this).registerReceiver(
notificationUpdateReceiver,
IntentFilter(PushNotificationApiHelper.ACTION_NEW_NOTIFICATIONS)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

@jaypanchal-13 onResume would be more ideal as it will restart listening when coming back

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Jignesh-dimagi It is already added in onResume

import android.os.Bundle
import org.commcare.utils.PushNotificationApiHelper

abstract class BaseDrawerActivity<T> : CommCareActivity<T>() {
Copy link
Contributor

Choose a reason for hiding this comment

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

@jaypanchal-13 Need to listen in ConnectActivity also to update the bell icon


suspend fun retrieveLatestPushNotifications(context: Context): Result<List<PushNotificationRecord>>{
val pushNotificationListResult = callPushNotificationApi(context)
suspend fun retrieveLatestPushNotifications(context: Context,isFromPNActivity: Boolean = false): Result<List<PushNotificationRecord>>{
Copy link
Contributor

@Jignesh-dimagi Jignesh-dimagi Oct 28, 2025

Choose a reason for hiding this comment

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

@jaypanchal-13 Let's not complicate the logic here. Let it throw the broadcast when new PNs but there will be no listeners, also in PushNotificationViewModel.kt change status to read after receiving the new PNs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Jignesh-dimagi status will be updated to unread after receiveing new PN's. Managed status in PNActivity

Copy link
Contributor

Choose a reason for hiding this comment

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

@jaypanchal-13 Whenver pushNotificationViewModel receives the latest notifications, it should set the icon to read and update the broadcast.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok seen that you have taken care in Push Notification Activity.

import androidx.lifecycle.LifecycleOwner
import androidx.localbroadcastmanager.content.LocalBroadcastManager
object NotificationBroadcastHelper {
const val ACTION_NEW_NOTIFICATIONS = "com.dimagi.NEW_NOTIFICATIONS_RECEIVED"
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 namespace this to the package instead org.commcare.dalvik.action.NEW_NOTIFICATION


override fun onDestroy() {
super.onDestroy()
NotificationPrefs.setNotificationAsRead(this)
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 be setting this only when we set the notifications list to adapter on UI as that's an indication that user has successfully seen the notification page. setNotificationAsRead should also be removed from all the other places in the code as just clicking the bell icon or menu item is not enough indication of success (if code fails after click but before showing notifications page, the status should remain un-read)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkForDrawerSetUp()
NotificationBroadcastHelper.registerForNotifications(this, this) {
Copy link
Contributor

Choose a reason for hiding this comment

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

should only happen if drawerController is not null



val isNotificationRead = NotificationPrefs.getNotificationReadStatus(activity)
val notificationIconRes =
Copy link
Contributor

Choose a reason for hiding this comment

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

Have a single method getNotificationIcon that checks for the unread status and give you the right icon and use it at both places - In drawer and on the action bar item.


object NotificationPrefs {
private const val PREF_NAME = "notification_prefs"
private const val KEY_PN_READ_STATUS = "pn_read_status"
Copy link
Contributor

Choose a reason for hiding this comment

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

replace pn -> notification

when {
notifications.isNotEmpty() -> {
pushNotificationAdapter.submitList(notifications)
NotificationPrefs.setNotificationAsRead(this)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: probably no functional side-effect but this should not ideally depend on notifications.isNotEmpty() but happen always

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 I still think onDestroy is better place for setNotificationAsRead. Because if user is in notification screen and new notification is fetched by clicking sync button on notification screen. It will not update pn icon as read. What are your views?

Copy link
Contributor

Choose a reason for hiding this comment

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

would not this observer get fired again when you click sync ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes isNotEmpty() if list is not empty and else if no item is fetched

Copy link
Contributor

Choose a reason for hiding this comment

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

so that's fine right ? Maybe I am not really understanding the issue 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.

Yes but In PushNotificationApiHelperon success of api call I updated status to unread. When sync btn is clicked and api is called again so it makes status unread even if I make it status read on this. So When placed setNotificationAsRead in onDestroy it updates status correctly

Copy link
Contributor

Choose a reason for hiding this comment

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

When sync btn is clicked and api is called again so it makes status unread even if I make it status read on this

We should be marking it read after the API call and my sense is that your observer gets triggered after the API call otherwise user will never see the updated data from the API. if it's not happening through observer, can you point out how are we handling data refresh when the network fetch results in new notifications ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it is behaving same. Just an error of sequence was there update api is also called on success of get pn api and after calling update api again process for get api was continued because continuation.resumeWith is added in update pn api.

snackbar.show();
}

public static int getNotificationIcon(Context context) {
Copy link
Contributor

Choose a reason for hiding this comment

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

don't think this is the right class for this to go, think NotificationUtil would be a better choice here.

shubham1g5
shubham1g5 previously approved these changes Oct 29, 2025
@shubham1g5
Copy link
Contributor

@jaypanchal-13 Can you also try to satisfy Kotlin lint errors on this PR (you can ignore the java ones)

@shubham1g5 shubham1g5 added the skip-integration-tests Skip android tests. label Oct 29, 2025
@jaypanchal-13
Copy link
Contributor Author

@jaypanchal-13 Can you also try to satisfy Kotlin lint errors on this PR (you can ignore the java ones)

@shubham1g5 This PR does not have. Do you want me to address lint for kotlin files changed in this PR?

@shubham1g5
Copy link
Contributor

@jaypanchal-13 right, you can see the errors here

@jaypanchal-13
Copy link
Contributor Author

@damagatchi retest this please

@shubham1g5 shubham1g5 merged commit 3e9af2a into master Oct 30, 2025
7 of 9 checks passed
@shubham1g5 shubham1g5 deleted the CCCT-1839-pm-read-unread-status branch October 30, 2025 09:04
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.

6 participants