Skip to content

Fix background network calls on Android 15 and higher#3525

Open
avazirna wants to merge 19 commits intomasterfrom
vaz/fix-background-network-calls-on-Android15-and-higher
Open

Fix background network calls on Android 15 and higher#3525
avazirna wants to merge 19 commits intomasterfrom
vaz/fix-background-network-calls-on-Android15-and-higher

Conversation

@avazirna
Copy link
Contributor

@avazirna avazirna commented Feb 3, 2026

Product Description

This PR addresses an issue where network requests triggered by AsyncTasks are interrupted when CommCare goes to the background on devices running Android 15 and higher. This behavior is due the background network access restrictions introduced in that version. As a result, the app installation feature was impacted as demonstrated in the video below:

app-installation-issue-on-android15.mp4

Technical Summary

After some tests, it was noticed that the restrictions are not applied when CommCareSessionService is running, and that the affected features are those accessible when the service is not running. Based on this, the chosen approach was to implement a service, NetworkNotificationService, that could run in parallel to those features.

It's important to note that this is a temporary solution. The scope of these restrictions seem to be expanding, and as a result this approach mayb be rejected by the Google Play Store. Additionally, AsyncTasks were deprecated in Android 11, so the long term goal is to replace them with components align with modern Android development best practices.

Feature Flag

Safety Assurance

Safety story

This was successfully tested locally.

QA Plan

I plan to send this to QA for regression testing.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

Adds a new foreground service, org.commcare.services.NetworkNotificationService, and the android.permission.FOREGROUND_SERVICE_DATA_SYNC permission in the Android manifest (service declared twice). Introduces a Kotlin service implementing foreground notification lifecycle and StateFlow task tracking. Integrates service usage into task execution: CommCareTask gains flags and helper intents to start/update/stop the service; ResourceEngineTask enables the flag and sets a progress text key. Adds string resources and translation keys for notification text.

Sequence Diagram

sequenceDiagram
    participant RT as ResourceEngineTask
    participant CCT as CommCareTask
    participant App as CommCareApplication
    participant NNS as NetworkNotificationService
    participant NM as NotificationManager

    RT->>CCT: execute() (runNotificationService=true)
    CCT->>CCT: onPreExecute()\nshouldRunNetworkNotificationService()
    CCT->>App: startForegroundService(start Intent)
    App->>NNS: deliver Intent -> onCreate/onStartCommand(START)
    NNS->>NM: build & notify(initial notification)

    CCT->>CCT: doInBackground() / progress updates
    CCT->>App: startService(update Intent with progress)
    App->>NNS: onStartCommand(UPDATE)
    NNS->>NM: notify(updated progress)

    CCT->>App: startService(stop Intent) / onPostExecute()
    App->>NNS: onStartCommand(STOP) / onDestroy()
    NNS->>NM: cancel notification
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

Release Note, High Risk

Suggested reviewers

  • OrangeAndGreen
  • conroy-ricketts
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% 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 directly and clearly describes the main change: implementing a fix for background network calls on Android 15+, which is the core objective of this PR.
Description check ✅ Passed The PR description covers most required sections: Product Description (with context and video), Technical Summary (design rationale), Safety story (local testing), and QA Plan. However, the Feature Flag section is empty/skipped, and the Automated test coverage section is not 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 vaz/fix-background-network-calls-on-Android15-and-higher

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

🤖 Fix all issues with AI agents
In `@app/src/org/commcare/services/NetworkNotificationService.kt`:
- Around line 84-88: The onDestroy method calls the API-24 overload
stopForeground(int) which will crash on devices < API 24; replace the direct
call with a runtime SDK check in onDestroy (method name onDestroy) that calls
stopForeground(STOP_FOREGROUND_REMOVE) only when Build.VERSION.SDK_INT >=
Build.VERSION_CODES.N and otherwise calls the legacy stopForeground(boolean)
overload (e.g. stopForeground(true) to match removal), then set isServiceRunning
= false and call super.onDestroy(); keep or remove the `@RequiresApi` annotation
as desired since the runtime check provides the actual protection.
🧹 Nitpick comments (1)
app/src/org/commcare/tasks/templates/CommCareTask.java (1)

153-168: Use the PROGRESS_INTENT_EXTRA constant to avoid drift.

♻️ Suggested change
-        return getNotificationIntent().setAction(UPDATE_PROGRESS_NOTIFICATION_ACTION)
-                .putExtra("progress", (int[])value);
+        return getNotificationIntent().setAction(UPDATE_PROGRESS_NOTIFICATION_ACTION)
+                .putExtra(NetworkNotificationService.PROGRESS_INTENT_EXTRA, (int[])value);

Comment on lines 84 to 88
@RequiresApi(Build.VERSION_CODES.N)
override fun onDestroy() {
stopForeground(STOP_FOREGROUND_REMOVE)
isServiceRunning = false
super.onDestroy()
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "build.gradle*" -o -name "*.kts" | head -20

Repository: dimagi/commcare-android

Length of output: 140


🏁 Script executed:

rg "minSdk" -g "build.gradle*" -g "*.kts" --max-count=20

Repository: dimagi/commcare-android

Length of output: 171


🏁 Script executed:

rg "stopForeground" app/src/org/commcare/services/NetworkNotificationService.kt -B 2 -A 2

Repository: dimagi/commcare-android

Length of output: 243


🌐 Web query:

Android Service stopForeground API documentation int parameter

💡 Result:

Service.stopForeground(int notificationBehavior) takes an int “notification behavior” flag that controls what happens to the foreground-service notification when the service is removed from the foreground state:

  • Service.STOP_FOREGROUND_REMOVE: cancel/remove the notification immediately. [1]
  • Service.STOP_FOREGROUND_DETACH: detach the notification from the service; the notification remains shown (and if it was deferred, it is posted immediately). [1]
  • Service.STOP_FOREGROUND_LEGACY (value 0): legacy behavior—notification stays until the service is fully stopped/destroyed, then is cancelled. [1]

Also note: stopForeground(boolean removeNotification) is deprecated in API 33; Android recommends calling stopForeground(int) with either STOP_FOREGROUND_REMOVE or STOP_FOREGROUND_DETACH explicitly. [1]

If you’re using AndroidX for compatibility helpers, the same flags exist in androidx.core.app.ServiceCompat (STOP_FOREGROUND_REMOVE = 1, STOP_FOREGROUND_DETACH = 2). [2]

Sources:
[1] Android Developers — android.app.Service API reference (stopForeground docs) (developer.android.com)
[2] Android Developers — androidx.core.app.ServiceCompat API reference (developer.android.com)

Citations:


🌐 Web query:

stopForeground int overload API level added Android 24

💡 Result:

Service.stopForeground(int flags) (the int overload) was added in API level 24 (Android 7.0). It was introduced along with flags like STOP_FOREGROUND_REMOVE and STOP_FOREGROUND_DETACH. [1], [2]


🌐 Web query:

Service.stopForeground boolean overload API level Android compatibility

💡 Result:

  • Service.stopForeground(boolean removeNotification) was added in API level 5 (Android 2.0) and is deprecated in API level 33 (Android 13). The boolean maps to legacy behaviors (true ≈ remove notification; false ≈ legacy/keep behavior). [1][2]
  • Service.stopForeground(int notificationBehavior) was added in API level 24 (Android 7.0). Use STOP_FOREGROUND_REMOVE to remove the notification, or STOP_FOREGROUND_DETACH to keep it shown but detach it from the service lifecycle. [1][3]

Practical compatibility pattern:

if (Build.VERSION.SDK_INT >= 24) {
  stopForeground(Service.STOP_FOREGROUND_REMOVE); // or STOP_FOREGROUND_DETACH
} else {
  stopForeground(true); // legacy
}

This avoids calling the int overload on API < 24, and avoids the deprecated boolean API on newer devices (and when targeting/compiling newer SDKs). [1][2][3]

Sources:
[1] Android Service API reference (stopForeground overloads, API levels, deprecation) https://developer.android.google.cn/reference/android/app/Service
[2] Android API diff (API 33: stopForeground(boolean) now deprecated) https://developer.android.com/sdk/api_diff/33/changes/android.app.Service
[3] Android Service API reference (STOP_FOREGROUND_* constants, API levels) https://developer.android.com/reference/android/app/Service


Guard stopForeground(int) for API < 24 with runtime check.

The stopForeground(int) overload was added in API 24, but your minSdk is 23. The @RequiresApi annotation provides no runtime protection—it only affects lint checks. On API 23 devices, this will crash with NoSuchMethodError. Add a runtime SDK check and fall back to the legacy stopForeground(boolean) overload for older APIs.

🛠️ Proposed fix
-    `@RequiresApi`(Build.VERSION_CODES.N)
-    override fun onDestroy() {
-        stopForeground(STOP_FOREGROUND_REMOVE)
+    override fun onDestroy() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            stopForeground(STOP_FOREGROUND_REMOVE)
+        } else {
+            stopForeground(true)
+        }
         isServiceRunning = false
         super.onDestroy()
     }
🤖 Prompt for AI Agents
In `@app/src/org/commcare/services/NetworkNotificationService.kt` around lines 84
- 88, The onDestroy method calls the API-24 overload stopForeground(int) which
will crash on devices < API 24; replace the direct call with a runtime SDK check
in onDestroy (method name onDestroy) that calls
stopForeground(STOP_FOREGROUND_REMOVE) only when Build.VERSION.SDK_INT >=
Build.VERSION_CODES.N and otherwise calls the legacy stopForeground(boolean)
overload (e.g. stopForeground(true) to match removal), then set isServiceRunning
= false and call super.onDestroy(); keep or remove the `@RequiresApi` annotation
as desired since the runtime check provides the actual protection.

@avazirna avazirna force-pushed the vaz/fix-background-network-calls-on-Android15-and-higher branch from 96a93cb to 1e9e829 Compare February 3, 2026 12:49
@avazirna avazirna force-pushed the vaz/fix-background-network-calls-on-Android15-and-higher branch from 1e9e829 to 9e6c39d Compare February 3, 2026 12:52
@avazirna avazirna self-assigned this Feb 3, 2026
@conroy-ricketts
Copy link
Contributor

@avazirna

What's the LOE on the better long-term solution? Trying to get a sense of why we wouldn't do that instead.

And what's the rationale for doing this knowing that it may be rejected by the Play Store anyways?

@conroy-ricketts
Copy link
Contributor

Also, is there a ticket for this?

Comment on lines 1021 to 1023
network.requests.starting=Starting network requests
network.requests.running=Running network requests
network.requests.progress=Completed: ${0}/${1}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not super user-friendly and ideally we should show contextual messages here depending on the task in progress, for eg. "Update in progress" and less ideally change it to a generic but more user friendly messaging like "Downloading data.."

Comment on lines +71 to +73
val activityToLaunch = Intent(this, DispatchActivity::class.java)
activityToLaunch.setAction("android.intent.action.MAIN")
activityToLaunch.addCategory("android.intent.category.LAUNCHER")
Copy link
Contributor

Choose a reason for hiding this comment

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

  • is not this already a default behaviour for notifications click ?
  • Is it possible to not do anything on notification click instead ? I think it's just a contextual message and it might not be good UX for users if they move out of "Form Entry" screen on clicking these 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.

  • is not this already a default behaviour for notifications click ?

This is for to set the content intent, without it, there won't be any action if the user taps the notification. Considering that this notification is mainly for when CommCare goes to the background, it makes sense to me to offer the user a way to go go back.

Copy link
Contributor

Choose a reason for hiding this comment

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

But the notification will appear even if the app is not backgrounded right ?

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 think it's just a contextual message and it might not be good UX for users if they move out of "Form Entry" screen on clicking these notifications.

Tapping the notification shouldn't make the users exit "Form entry". This is the same behavior that occurs when users tap the session notification.

Copy link
Contributor

Choose a reason for hiding this comment

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

Tapping the notification shouldn't make the users exit "Form entry".

Interesting, curious how is that controlled in the 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.

Not directly.DispatchActivity.class is a root activity and already present in the task’s stack, so a new instance is not created. But the intended behavior here is that when the app is in the background, the system brings it to the foreground to deliver the pending intent.

val pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
val pendingIntent = PendingIntent.getActivity(this, 0, activityToLaunch, pendingIntentFlags)

return NotificationCompat
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible for these notifications to be sticky (non-dismissable) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should be already, the ongoing flag is set to true. Unless the comment is about the service itself.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good, didn't realise this flag is used to communicate UX on a notification being "user-dismissable". But do want to do double confirm that user is not able to dismiss this notification ?

)

@RequiresApi(Build.VERSION_CODES.N)
override fun onDestroy() {
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 also auto-dismiss the notifications started by this service.

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 believe we want to make them sticky, right? Are you referring to the FLAG_AUTO_CANCEL?

Copy link
Contributor

Choose a reason for hiding this comment

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

We want to make them sticky but only for the duration of the service but once service completes, the notification should auto disappear.

Are you referring to the FLAG_AUTO_CANCEL?

No, we don't really want user to be able to dismiss the notification.

Comment on lines 153 to 155
if (shouldUpdateNetworkNotificationProgress(values)) {
CommCareApplication.instance().startService(getNotificationIntentProgress(values[0]));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Think we handle progress differently in different tasks and this will need to be cross-checked for every task , I think it's better to not show any progress in the notification instead by default and have individual tasks implement progress updates if required. Just saying "Download in Progress" should be sufficient here for most cases.

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, I battled with this myself and may have overcomplicated it. The intention was to address immediate needs and refactor as we expand this service to other tasks. But I'm onboard with the idea of starting small.

Comment on lines 104 to 106
if (NetworkNotificationService.Companion.isServiceRunning()) {
CommCareApplication.instance().stopService(getNotificationIntent());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This would not work anytime we have more than one Async tasks in parallel as any task terminating will also stop the service for the in progress task.

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 have opted to store the running tasks IDs in a List and call stopSelf() once the list is empty - 508fa39

@avazirna
Copy link
Contributor Author

Also, is there a ticket for this?

Yes, there is, let me link it to the PR.

@avazirna
Copy link
Contributor Author

What's the LOE on the better long-term solution? Trying to get a sense of why we wouldn't do that instead.

@conroy-ricketts With the CommCareSessionService retirement work, we are going to have several AsyncTasks in the same situation. The goal was to find a generic solution that could allow us proper time to make these changes, especially considering that they impact core features of CommCare

@avazirna
Copy link
Contributor Author

And what's the rationale for doing this knowing that it may be rejected by the Play Store anyways?

I don't expect to be rejected now, but it's a risk that we need to monitor.

@avazirna avazirna force-pushed the vaz/fix-background-network-calls-on-Android15-and-higher branch from 66eec0d to bdb2f0f Compare February 13, 2026 17:01
@avazirna avazirna requested a review from shubham1g5 February 16, 2026 20:40
@shubham1g5
Copy link
Contributor

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

✅ Actions performed

Full review triggered.

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

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/tasks/templates/CommCareTask.java (1)

73-88: ⚠️ Potential issue | 🟠 Major

Service is not stopped when a task is cancelled — potential service leak.

onCancelled() does not send a STOP_NOTIFICATION_ACTION to the NetworkNotificationService. If a task that opted into the notification service is cancelled, its task ID remains registered in the service, preventing the auto-shutdown (stopSelf() triggered when the task list becomes empty). The service will keep running indefinitely.

Suggested fix
     `@Override`
     protected void onCancelled() {
         super.onCancelled();
 
         synchronized (connectorLock) {
             CommCareTaskConnector<Receiver> connector = getConnector();
 
             if (connector != null) {
                 connector.startTaskTransition();
                 connector.stopBlockingForTask(getTaskId());
                 connector.taskCancelled();
                 handleCancellation(connector.getReceiver());
                 connector.stopTaskTransition(taskId);
             }
         }
+
+        if (NetworkNotificationService.Companion.isServiceRunning()) {
+            CommCareApplication.instance().startForegroundService(getNotificationStopIntent());
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/tasks/templates/CommCareTask.java` around lines 73 - 88,
onCancelled() must notify NetworkNotificationService that this task is no longer
active to avoid a service leak; modify CommCareTask.onCancelled to send the
STOP_NOTIFICATION_ACTION intent to NetworkNotificationService (including the
task id from getTaskId()) when a connector is non-null and the task opted into
notifications—do this alongside the existing
connector.stopBlockingForTask(getTaskId())/connector.taskCancelled() calls
(before or after them) so the service can remove the task and possibly call
stopSelf(); reference the onCancelled method, getConnector(), getTaskId(),
connector.stopBlockingForTask(...) and
NetworkNotificationService.STOP_NOTIFICATION_ACTION when locating where to add
the stop-intent dispatch.
🧹 Nitpick comments (3)
app/AndroidManifest.xml (1)

343-347: Consider adding android:exported="false" explicitly.

The NetworkNotificationService declaration omits the android:exported attribute. While it defaults to false when no intent-filter is present, other services in this manifest (e.g., AudioRecordingService at line 350) explicitly set it. Being explicit is preferable for consistency and to prevent accidental exposure if an intent-filter is added later.

Suggested change
         <service
             android:enabled="true"
+            android:exported="false"
             android:foregroundServiceType="dataSync"
             android:name="org.commcare.services.NetworkNotificationService">
         </service>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/AndroidManifest.xml` around lines 343 - 347, The
NetworkNotificationService declaration
(android:name="org.commcare.services.NetworkNotificationService") lacks an
explicit android:exported attribute; update the <service> element for
NetworkNotificationService to include android:exported="false" to match other
services (e.g., AudioRecordingService) and prevent accidental external exposure
if an intent-filter is added later.
app/src/org/commcare/services/NetworkNotificationService.kt (1)

93-93: super.onStartCommand() returns START_STICKY, which will restart the service without the original intent.

If the system kills and restarts this service, onStartCommand will be called with a null intent, resulting in no action taken and an empty task list — the service will then stop itself immediately. While not a crash, returning START_NOT_STICKY would be more appropriate for a task-driven service that should not be restarted without an explicit command.

Suggested fix
-        return super.onStartCommand(intent, flags, startId)
+        return START_NOT_STICKY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/services/NetworkNotificationService.kt` at line 93, The
service currently returns super.onStartCommand(...), which yields START_STICKY;
change NetworkNotificationService.onStartCommand to return START_NOT_STICKY
instead so the system won't restart the service without the original intent.
Locate the onStartCommand method in class NetworkNotificationService and replace
the final return expression (currently calling super.onStartCommand(intent,
flags, startId)) with the Service constant START_NOT_STICKY; keep existing
intent-null checks/early exits as-is so the method still stops itself when no
work is provided.
app/src/org/commcare/tasks/templates/CommCareTask.java (1)

139-142: API level gate uses UPSIDE_DOWN_CAKE (API 34), but onCreate in the service handles API < 34 too.

shouldRunNetworkNotificationService() gates on Build.VERSION_CODES.UPSIDE_DOWN_CAKE (API 34), meaning the service is never started on API 33 or below. Meanwhile, NetworkNotificationService.onCreate() has a branch for < UPSIDE_DOWN_CAKE (lines 52-53 of the service). That branch is dead code since the service is never started on those API levels. This is fine functionally (the fix targets Android 15+ restrictions), but the dead code in the service is misleading — consider either documenting the intent or removing the else branch from the service's onCreate.

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

In `@app/src/org/commcare/tasks/templates/CommCareTask.java` around lines 139 -
142, shouldRunNetworkNotificationService() gates starting the service to API
34+, but NetworkNotificationService.onCreate() still contains an else branch for
< UPSIDE_DOWN_CAKE which is dead code; either remove that else branch and
simplify onCreate() to only handle the API >= UPSIDE_DOWN_CAKE path, or if you
intend to support older APIs, update shouldRunNetworkNotificationService() to
allow starting the service on lower API levels. Locate
shouldRunNetworkNotificationService() and NetworkNotificationService.onCreate()
and then either (A) delete the < UPSIDE_DOWN_CAKE branch and add a brief comment
in onCreate() clarifying the service is only started on API 34+, or (B) change
shouldRunNetworkNotificationService() to not require UPSIDE_DOWN_CAKE so the
onCreate() else branch is reachable — pick the option matching intended
behavior.
🤖 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/assets/locales/android_translatable_strings.txt`:
- Around line 1021-1023: The localization keys used by
NetworkNotificationService.kt ("network.requests.starting" and
"network.requests.running") do not match the keys in the translations file
(network.notification.service.starting / .running); fix this by making them
identical: either rename the keys in the translation file to
"network.requests.starting" and "network.requests.running" or change the string
literals in NetworkNotificationService.kt to
"network.notification.service.starting" and
"network.notification.service.running" so that Localization.get() can resolve
the texts (update the three symbols: the two keys in the translations and the
occurrences in NetworkNotificationService.kt at the places using those
literals).

In `@app/src/org/commcare/services/NetworkNotificationService.kt`:
- Around line 80-85: The fallback localization key used when building the
notification is incorrect: in NetworkNotificationService (inside the
notificationManager.notify call that calls buildNotification), replace the
hard-coded fallback string "network.requests.running" with the correct key
"network.notification.service.running" where the intent extra
PROGRESS_TEXT_KEY_INTENT_EXTRA is read, so buildNotification(...) receives the
proper default localization key (keep the existing null-coalescing logic).
- Around line 101-108: registerTaskId can add duplicates when update is false
because the presence check is skipped; change registerTaskId (and its use of
_taskIds.update) to always avoid adding an existing taskId. Specifically, in
registerTaskId replace the current early-return logic with an atomic check
inside the _taskIds.update lambda: if current.contains(taskId) return current
else return current + taskId (so use current.contains/taskId and _taskIds.update
to decide), ensuring duplicates are never appended and removeTaskId can fully
clear entries.
- Around line 49-53: The localization keys used in buildNotification calls are
incorrect; update the string keys passed to buildNotification (and any direct
Localization.get usages) from "network.requests.starting" /
"network.requests.running" to the correct keys
"network.notification.service.starting" and
"network.notification.service.running" so they match
android_translatable_strings.txt—search for buildNotification(...) and any
Localization.get(...) in NetworkNotificationService (and usages around
startForeground/NON-foreground branches) and replace the keys accordingly.

---

Outside diff comments:
In `@app/src/org/commcare/tasks/templates/CommCareTask.java`:
- Around line 73-88: onCancelled() must notify NetworkNotificationService that
this task is no longer active to avoid a service leak; modify
CommCareTask.onCancelled to send the STOP_NOTIFICATION_ACTION intent to
NetworkNotificationService (including the task id from getTaskId()) when a
connector is non-null and the task opted into notifications—do this alongside
the existing
connector.stopBlockingForTask(getTaskId())/connector.taskCancelled() calls
(before or after them) so the service can remove the task and possibly call
stopSelf(); reference the onCancelled method, getConnector(), getTaskId(),
connector.stopBlockingForTask(...) and
NetworkNotificationService.STOP_NOTIFICATION_ACTION when locating where to add
the stop-intent dispatch.

---

Duplicate comments:
In `@app/src/org/commcare/services/NetworkNotificationService.kt`:
- Around line 130-136: The call to stopForeground(STOP_FOREGROUND_REMOVE) in
onDestroy requires API 24 but is currently annotated with `@RequiresApi`; add a
brief explanatory comment inside onDestroy (near stopForeground) stating that
runtime startup is already gated by
CommCareTask.shouldRunNetworkNotificationService() (which prevents this service
from running on API < 34), so this code path is unreachable on older SDKs and a
runtime check is intentionally omitted; reference stopForeground, onDestroy,
`@RequiresApi`, and CommCareTask.shouldRunNetworkNotificationService in the
comment for future maintainers.

---

Nitpick comments:
In `@app/AndroidManifest.xml`:
- Around line 343-347: The NetworkNotificationService declaration
(android:name="org.commcare.services.NetworkNotificationService") lacks an
explicit android:exported attribute; update the <service> element for
NetworkNotificationService to include android:exported="false" to match other
services (e.g., AudioRecordingService) and prevent accidental external exposure
if an intent-filter is added later.

In `@app/src/org/commcare/services/NetworkNotificationService.kt`:
- Line 93: The service currently returns super.onStartCommand(...), which yields
START_STICKY; change NetworkNotificationService.onStartCommand to return
START_NOT_STICKY instead so the system won't restart the service without the
original intent. Locate the onStartCommand method in class
NetworkNotificationService and replace the final return expression (currently
calling super.onStartCommand(intent, flags, startId)) with the Service constant
START_NOT_STICKY; keep existing intent-null checks/early exits as-is so the
method still stops itself when no work is provided.

In `@app/src/org/commcare/tasks/templates/CommCareTask.java`:
- Around line 139-142: shouldRunNetworkNotificationService() gates starting the
service to API 34+, but NetworkNotificationService.onCreate() still contains an
else branch for < UPSIDE_DOWN_CAKE which is dead code; either remove that else
branch and simplify onCreate() to only handle the API >= UPSIDE_DOWN_CAKE path,
or if you intend to support older APIs, update
shouldRunNetworkNotificationService() to allow starting the service on lower API
levels. Locate shouldRunNetworkNotificationService() and
NetworkNotificationService.onCreate() and then either (A) delete the <
UPSIDE_DOWN_CAKE branch and add a brief comment in onCreate() clarifying the
service is only started on API 34+, or (B) change
shouldRunNetworkNotificationService() to not require UPSIDE_DOWN_CAKE so the
onCreate() else branch is reachable — pick the option matching intended
behavior.

Comment on lines +1021 to +1023
network.notification.service.starting=Starting network service
network.notification.service.running=Running network requests...
network.notification.service.downloading.files=Downloading application files...
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 | 🔴 Critical

Localization key mismatch — service uses different keys than those defined here.

The translation file defines:

  • network.notification.service.starting
  • network.notification.service.running

But NetworkNotificationService.kt uses:

  • "network.requests.starting" (lines 49, 53)
  • "network.requests.running" (line 83)

These keys don't match, so Localization.get() will fail to find the correct text at runtime. Either the keys here or the string literals in the service need to be aligned.

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

In `@app/assets/locales/android_translatable_strings.txt` around lines 1021 -
1023, The localization keys used by NetworkNotificationService.kt
("network.requests.starting" and "network.requests.running") do not match the
keys in the translations file (network.notification.service.starting /
.running); fix this by making them identical: either rename the keys in the
translation file to "network.requests.starting" and "network.requests.running"
or change the string literals in NetworkNotificationService.kt to
"network.notification.service.starting" and
"network.notification.service.running" so that Localization.get() can resolve
the texts (update the three symbols: the two keys in the translations and the
occurrences in NetworkNotificationService.kt at the places using those
literals).

Comment on lines 49 to 53
buildNotification("network.requests.starting"),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
} else {
startForeground(NETWORK_NOTIFICATION_ID, buildNotification("network.requests.starting"))
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 | 🔴 Critical

Localization keys in this service don't match the keys defined in android_translatable_strings.txt.

The code uses "network.requests.starting" (line 49/53) and "network.requests.running" (line 83), but the translation file defines "network.notification.service.starting" and "network.notification.service.running". This will cause Localization.get() to fail or return the raw key string at runtime.

Suggested fix
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
             startForeground(
                 NETWORK_NOTIFICATION_ID,
-                buildNotification("network.requests.starting"),
+                buildNotification("network.notification.service.starting"),
                 ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
             )
         } else {
-            startForeground(NETWORK_NOTIFICATION_ID, buildNotification("network.requests.starting"))
+            startForeground(NETWORK_NOTIFICATION_ID, buildNotification("network.notification.service.starting"))
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/services/NetworkNotificationService.kt` around lines 49
- 53, The localization keys used in buildNotification calls are incorrect;
update the string keys passed to buildNotification (and any direct
Localization.get usages) from "network.requests.starting" /
"network.requests.running" to the correct keys
"network.notification.service.starting" and
"network.notification.service.running" so they match
android_translatable_strings.txt—search for buildNotification(...) and any
Localization.get(...) in NetworkNotificationService (and usages around
startForeground/NON-foreground branches) and replace the keys accordingly.

Comment on lines 80 to 85
notificationManager.notify(
NETWORK_NOTIFICATION_ID,
buildNotification(
intent.getStringExtra(PROGRESS_TEXT_KEY_INTENT_EXTRA) ?: "network.requests.running",
),
)
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 | 🔴 Critical

Same localization key mismatch for the fallback key.

"network.requests.running" should be "network.notification.service.running" to match the translation file.

Suggested fix
                 notificationManager.notify(
                     NETWORK_NOTIFICATION_ID,
                     buildNotification(
-                        intent.getStringExtra(PROGRESS_TEXT_KEY_INTENT_EXTRA) ?: "network.requests.running",
+                        intent.getStringExtra(PROGRESS_TEXT_KEY_INTENT_EXTRA) ?: "network.notification.service.running",
                     ),
                 )
📝 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
notificationManager.notify(
NETWORK_NOTIFICATION_ID,
buildNotification(
intent.getStringExtra(PROGRESS_TEXT_KEY_INTENT_EXTRA) ?: "network.requests.running",
),
)
notificationManager.notify(
NETWORK_NOTIFICATION_ID,
buildNotification(
intent.getStringExtra(PROGRESS_TEXT_KEY_INTENT_EXTRA) ?: "network.notification.service.running",
),
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/services/NetworkNotificationService.kt` around lines 80
- 85, The fallback localization key used when building the notification is
incorrect: in NetworkNotificationService (inside the notificationManager.notify
call that calls buildNotification), replace the hard-coded fallback string
"network.requests.running" with the correct key
"network.notification.service.running" where the intent extra
PROGRESS_TEXT_KEY_INTENT_EXTRA is read, so buildNotification(...) receives the
proper default localization key (keep the existing null-coalescing logic).

Comment on lines +101 to +108
private fun registerTaskId(
taskId: Int,
update: Boolean,
) {
if (taskId == -1 || (update && taskId in _taskIds.value)) return

_taskIds.update { current -> current + taskId }
}
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 17, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

registerTaskId allows duplicate entries when update is false.

When called from START_NOTIFICATION_ACTION (update = false), the duplicate check is skipped, so the same taskId can be appended multiple times to the list. Each duplicate requires a corresponding removeTaskId call to fully clear it (since List.minus only removes the first occurrence). This could prevent the service from stopping.

Suggested fix — always prevent duplicates
     private fun registerTaskId(
         taskId: Int,
         update: Boolean,
     ) {
-        if (taskId == -1 || (update && taskId in _taskIds.value)) return
+        if (taskId == -1 || taskId in _taskIds.value) return
 
         _taskIds.update { current -> current + taskId }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/org/commcare/services/NetworkNotificationService.kt` around lines 101
- 108, registerTaskId can add duplicates when update is false because the
presence check is skipped; change registerTaskId (and its use of
_taskIds.update) to always avoid adding an existing taskId. Specifically, in
registerTaskId replace the current early-return logic with an atomic check
inside the _taskIds.update lambda: if current.contains(taskId) return current
else return current + taskId (so use current.contains/taskId and _taskIds.update
to decide), ensuring duplicates are never appended and removeTaskId can fully
clear entries.

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 expected, as we need to cater for legitimate duplicates, considering scenarios where multiple instances of the same AsyncTask may run at the same time.

Copy link
Contributor

Choose a reason for hiding this comment

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

@avazirna, thank you for the clarification! That makes sense—tracking each running instance separately (even when they share the same task ID) ensures that the service only stops once all concurrent instances have completed. The List.minus operation removing one occurrence at a time aligns perfectly with this design.


🧠 Learnings used
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.

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.

3 participants