Skip to content

Conversation

@thomasttvo
Copy link
Collaborator

@thomasttvo thomasttvo commented Oct 17, 2025

Fixes notification race condition where queued uploads could overwrite the active upload's notification settings. The bug occurred because each Upload object carried its own notification config—when a Wi-Fi-only upload was queued while a regular upload was running, the queued upload could touch the shared system notification and apply its wifiOnly=true preference before actually executing.

Centralization solves this by making UploadNotification the single source of truth for notification state. It tracks exactly one activeUpload at a time (synchronized), and notification content is built by reading getActiveUpload()?.wifiOnly rather than arbitrary upload configs. Queued uploads can exist in memory with their settings, but they can't affect the notification until they call setActiveUpload() when they start executing. This eliminates the race condition—only the actively running upload determines what the user sees.

Test Plan:

  1. Start a regular upload (non-Wi-Fi-only)
  2. While it's running, queue a Wi-Fi-only upload
  3. Verify notification shows correct connectivity preference for the actively running upload (not the queued one)
  4. Wait for first upload to complete, verify notification updates to reflect the now-active Wi-Fi-only upload
  5. Test notification persistence across app kills and device restarts
  6. Verify both uploads complete successfully

Note

Introduces centralized, connectivity-aware Android notification handling and a new initialization flow.

  • Notification refactor: New `UploadNotification` singleton builds/updates the foreground notification based on active upload and `Connectivity.fetch(...)`; removed per-upload notification fields from `Upload` and related logic from `UploadWorker`.
  • Connectivity utility: New `Connectivity` helper encapsulates internet/Wi‑Fi checks; `UploadWorker` uses it for retries/early exits.
  • Initialization API: New `initialize(options)` exposed via `UploaderModule` and JS; sets global Android notification options (IDs/titles/channel) once.
  • Type changes: Added `AndroidInitializationOptions`; removed `android` options from `UploadOptions`; `startUpload` awaits prior `initialize`.
  • Example + version: Example app updated to call `Upload.initialize(...)`; package version bumped to `7.6.0`.

Written by Cursor Bugbot for commit 7063263. Configure here.

NoWifi, NoInternet, Ok;

companion object {
fun fetch(context: Context, wifiOnly: Boolean): Connectivity {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved from UploadWorker.ts no logical change

}

// builds the notification required to enable Foreground mode
fun build(context: Context): Notification {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved from UploadWorker.ts. Not much has changed except for val wifiOnly = getActiveUpload()?.wifiOnly ?: false

if (this.activeUpload?.id == upload.id) this.activeUpload = null
}

fun setOptions(opts: ReadableMap) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

these options are moved over from the Upload class, so they are now global options instead of per-Upload options

Copy link

@elliottkember elliottkember left a comment

Choose a reason for hiding this comment

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

LGTM

setProgress(undefined);
console.log('Upload error!', err);
});
for (let i = 0; i < 100; i++) {

Choose a reason for hiding this comment

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

Can we add a comment explaining the magic number 100 and why we have to perform Upload.startUpload 100 times?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ah this is just the test app, but it's good to add a comment still

thomasvo and others added 6 commits December 11, 2025 16:42
- Name response variable in use block (UploadUtils.kt)
- Fix shadowed it in takeIf lambda (UploadUtils.kt)
- Rename set() to setIfNotNull() (UploadProgress.kt)
- Add complete() method to Progress class (UploadProgress.kt)
- Rename clearIfNeeded() to clearIfCompleted() (UploadProgress.kt)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@thomasttvo thomasttvo changed the base branch from thomas/simplify-progress to master January 21, 2026 01:41
@thomasttvo
Copy link
Collaborator Author

bugbot run

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

ios?: IOSOnlyUploadOptions;
} & RawUploadOptions;

type AndroidOnlyUploadOptions = {
Copy link

Choose a reason for hiding this comment

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

maxRetries option ignored in initialization

Medium Severity

The maxRetries property in AndroidInitializationOptions type is declared but never read by UploadNotification.setOptions(). Meanwhile, Upload.fromReadableMap() still expects maxRetries from per-upload options, but UploadOptions no longer includes this property. This makes maxRetries effectively non-configurable—users passing it to initialize() will have it silently ignored, and the default value of 5 will always be used.

Additional Locations (1)

Fix in Cursor Fix in Web


// Don't bother to run on an invalid network
if (!validateAndReportConnectivity()) return null
if (Connectivity.fetch(context, upload.wifiOnly) != Connectivity.Ok) return null
Copy link

Choose a reason for hiding this comment

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

Notification not updated when connectivity check fails

Medium Severity

When the connectivity check fails in upload(), the code returns null without updating the notification. Previously, validateAndReportConnectivity() always updated the notification to reflect connectivity state. Now, a wifi-only upload waiting for wifi shows "Uploading" instead of "Waiting for WiFi" because UploadNotification.build() uses the active upload's wifiOnly (defaulting to false when no active upload), and the notification is never updated when connectivity fails pre-semaphore.

Fix in Cursor Fix in Web

// mark as active upload for notification
UploadNotification.setActiveUpload(upload)
UploadNotification.update(context)

Copy link

Choose a reason for hiding this comment

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

Semaphore leak if notification update throws exception

High Severity

If UploadNotification.setActiveUpload() or UploadNotification.update() throws an exception after semaphore.acquire() but before entering the try block, the finally block containing semaphore.release() will never execute. With MAX_CONCURRENCY = 1, this would permanently leak the only permit, causing all subsequent uploads to hang indefinitely waiting for the semaphore. The old code had semaphore.acquire() immediately followed by try, avoiding this window.

Fix in Cursor Fix in Web

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants