Skip to content

Conversation

@SachinPremkumar
Copy link
Collaborator

@SachinPremkumar SachinPremkumar commented Jan 5, 2026

RCF-1278

Summary by CodeRabbit

  • New Features

    • Inline cron editor in Settings with validation, submit, and app reschedule/restart on apply.
    • Real-time job status (last sync / next run) via periodic polling.
    • Fetch permitted editable jobs and persist per-job cron/value overrides; new platform APIs to validate, modify, and read values.
    • Scheduling respects local overrides for job frequencies.
  • Bug Fixes

    • Improved lifecycle handling for background receivers and polling to avoid leaks.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 5, 2026

Walkthrough

Adds platform-channel APIs and UI to view/validate/modify per-job cron expressions; wires LocalConfigService through Android DI and clientmanager layers, persisting local overrides, emitting reschedule broadcasts, and introducing Flutter polling/state to surface job statuses and permit inline cron editing.

Changes

Cohort / File(s) Summary
Android DI & API wiring
android/app/src/main/java/io/mosip/registration_client/HostApiModule.java, android/clientmanager/src/main/java/io/mosip/registration/clientmanager/config/AppModule.java
Inject LocalConfigService into MasterDataSyncApi and JobManagerService provider; update factory/provider signatures and constructor invocations to pass the new dependency.
Android Sync API & MainActivity
android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java, android/app/src/main/java/io/mosip/registration_client/MainActivity.java
Add Sync API endpoints (getPermittedJobs, isValidCronExpression, modifyJobCronExpression, getValue) wired to LocalConfigService; emit and handle RESCHEDULE_JOB broadcast for cron changes; register/unregister receiver in MainActivity.
Clientmanager SPI / Impl & DAO changes
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/spi/LocalConfigService.java, .../service/LocalConfigServiceImpl.java, .../dao/LocalConfigDAO.java, .../dao/LocalConfigDAOImpl.java
Extend LocalConfigService SPI with getValue, modifyJob, getPermittedJobs; implement validation (CronExpressionParser), permission checks, DAO delegation, and add DAO signatures/impls for value retrieval and job modification.
LocalPreferences & SyncJobDef persistence
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalPreferencesDao.java, .../repository/LocalPreferencesRepository.java, .../dao/SyncJobDefDao.java, .../repository/SyncJobDefRepository.java
Add config-type-aware DAO/repository methods (findByIsDeletedFalseAndNameAndConfigType, findOneByApiName) and repository accessors to prevent cross-type contamination and enable lookup by API name.
Job scheduling / BatchJob / JobManager
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java, android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java
Thread LocalConfigService into JobManagerServiceImpl and BatchJob; add helper to prefer local cron overrides when computing sync frequency/interval; use override when scheduling.
Pigeon (platform channel)
pigeon/master_data_sync.dart
Add four new SyncApi methods for permitted jobs, cron validation/modification, and value retrieval.
Flutter SPI & Android platform impl
lib/platform_spi/sync_response_service.dart, lib/platform_android/sync_response_service_impl.dart
Add abstract methods and Android-side implementations for permitted jobs, cron validation/modification, and getValue with PlatformException-safe fallbacks.
Flutter provider (polling & state)
lib/provider/sync_provider.dart
Add periodic job polling (30s), JobStatus model, jobStatuses getter, start/stop polling and refreshJobStatuses to populate last/next run times and notify UI.
Flutter UI — Scheduled jobs editor
lib/ui/settings/widgets/scheduled_jobs_settings.dart
Convert ScheduledJobsSettings to StatefulWidget; add permitted-job gating, per-job inline cron editor with validation/submission, provider integration, and app restart trigger on successful cron change.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as Scheduled Jobs UI
    participant Provider as SyncProvider
    participant DartSvc as SyncResponseService (Dart)
    participant Pigeon as SyncApi (Pigeon)
    participant AndroidApi as MasterDataSyncApi
    participant Config as LocalConfigService
    participant JobMgr as JobManagerService

    User->>UI: Edit cron expression
    UI->>DartSvc: isValidCronExpression(cron)
    DartSvc->>Pigeon: isValidCronExpression
    Pigeon->>AndroidApi: isValidCronExpression
    AndroidApi->>AndroidApi: validate via CronExpressionParser
    AndroidApi-->>Pigeon: bool
    Pigeon-->>DartSvc: bool
    DartSvc-->>UI: validation result

    alt valid
        UI->>DartSvc: modifyJobCronExpression(jobId, cron)
        DartSvc->>Pigeon: modifyJobCronExpression
        Pigeon->>AndroidApi: modifyJobCronExpression
        AndroidApi->>Config: modifyJob(jobId, cron)
        Config->>Config: validate & persist
        AndroidApi->>JobMgr: refreshJobStatus(jobId)
        JobMgr->>Config: getSyncFrequency(job)
        Config-->>JobMgr: override cron if present
        AndroidApi-->>Pigeon: success
        Pigeon-->>DartSvc: success
        DartSvc-->>UI: success -> trigger app restart
        AndroidApi->>AndroidApi: send RESCHEDULE_JOB broadcast
        AndroidApi->>JobMgr: createBackgroundTask(job)
    else invalid
        UI->>UI: show error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • ase-101
  • MadhuMosip

Poem

🐇
I hop through crons both near and far,
Tweak a tick, nudge a nightly star,
Flutter hums and Android wakes,
Jobs rescheduled, no mistakes,
A rabbit's wink — new schedules are.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.76% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'RCF-1278: Editable Cron Job in Scheduled Job Settings' clearly summarizes the main change: enabling users to edit cron job expressions in the scheduled jobs settings UI, which is the primary feature across all modifications.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

Fix all issues with AI Agents 🤖
In
@android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java:
- Around line 93-113: modifyJob performs a soft-delete via
updateLocalPreference(LocalPreferences, ...) and then creates a new record via
saveLocalPreference(name, value, ...), but these are separate DB operations and
must be atomic; wrap both calls in a single transaction by annotating modifyJob
with @Transaction (or call database.runInTransaction { ... } around
updateLocalPreference and saveLocalPreference) to ensure both succeed or both
roll back, preventing inconsistent state or race conditions when
LocalPreferences is concurrently modified.

In
@android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java:
- Around line 43-46: The service method LocalConfigServiceImpl.modifyJob must
validate the cron expression before delegating to the DAO: inject
CronExpressionParser into LocalConfigServiceImpl, call it to validate the
incoming value and throw a clear exception (e.g., IllegalArgumentException or a
domain-specific ValidationException) when invalid, then call
localConfigDAO.modifyJob only for valid expressions. Also make
LocalConfigDAOImpl.modifyJob atomic to avoid race conditions by either marking
the method synchronized or, preferably, wrapping the read-check-write in a
database transaction with an appropriate lock (SELECT ... FOR UPDATE or
equivalent) so the read/validate/update sequence is executed atomically.

In @lib/ui/settings/widgets/scheduled_jobs_settings.dart:
- Around line 212-216: The code calls
_syncResponseService.modifyJobCronExpression with widget.job.id without
validating it; add a guard that checks widget.job.id for null or empty (e.g., if
(widget.job.id == null || widget.job.id!.isEmpty) ) before invoking
modifyJobCronExpression, and handle the case by returning early and surfacing an
error/notification (or logging) so modifyJobCronExpression is never called with
an empty id; update any surrounding try/catch flow to reflect the early return.
🧹 Nitpick comments (6)
lib/platform_spi/sync_response_service.dart (1)

35-37: LGTM! Consider error details beyond boolean returns.

The method signatures are appropriate for the cron job management feature. The modifyJobCronExpression method returns a boolean to indicate success/failure, which is simple and clear.

For enhanced error reporting in the UI, you might consider returning a result object that includes error messages or codes, but the current boolean approach is acceptable for a "Chill" review.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAO.java (1)

36-41: Rename parameter name to jobId for semantic clarity and document the exception-throwing behavior.

Parameter naming is inconsistent with the caller in MasterDataSyncApi.java:464, which passes jobId and cronExpression to a method that declares parameters as name and value. Renaming to jobId and cronExpression would improve code readability and consistency across the codebase.

Additionally, the implementation (in LocalConfigDAOImpl.java:109-111) throws exceptions on database errors, but the interface (LocalConfigDAO.java) does not document this with a @throws clause. Add @throws Exception to the method documentation to make the error handling contract explicit to callers.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)

80-91: Consider surfacing errors for critical configuration reads.

The getValue method catches and logs all exceptions but returns null, making it difficult for callers to distinguish between "configuration not found" and "database error occurred."

If configuration retrieval is critical for job execution, consider:

  • Rethrowing exceptions for database errors while still returning null for "not found" cases
  • Or documenting that null may indicate either scenario and callers should check logs

This is a minor point and depends on your error handling philosophy—current implementation may be intentional for fail-safe behavior.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java (1)

88-88: Consider using getSyncFrequency for consistency.

Line 88 still passes jobDef.getSyncFreq() directly to scheduleJob, while Line 173 now uses the new getSyncFrequency(jobDef) helper.

Although scheduleJob doesn't currently use the syncFreq parameter for actual scheduling (it uses hardcoded JOB_PERIODIC_SECONDS), using getSyncFrequency(jobDef) here would improve consistency across the codebase and make future refactoring easier if the scheduling logic changes to honor the sync frequency.

🔎 Proposed change for consistency
-        if (!isJobScheduled(jobId))
-            scheduleJob(jobId, jobDef.getApiName(), jobDef.getSyncFreq());
+        if (!isJobScheduled(jobId))
+            scheduleJob(jobId, jobDef.getApiName(), getSyncFrequency(jobDef));
android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java (1)

461-470: Consider validating cron expression before persisting.

The method does not validate the cron expression before modifying the job. While the UI calls isValidCronExpression first, adding server-side validation here provides defense in depth against invalid data being persisted.

🔎 Proposed fix to add validation
 @Override
 public void modifyJobCronExpression(@NonNull String jobId, @NonNull String cronExpression, @NonNull MasterDataSyncPigeon.Result<Boolean> result) {
     try {
+        if (!CronExpressionParser.isValidCronExpression(cronExpression)) {
+            result.success(false);
+            return;
+        }
         localConfigService.modifyJob(jobId, cronExpression);
         result.success(true);
     } catch (Exception e) {
         Log.e(TAG, "Failed to modify job cron expression", e);
         result.error(e);
     }
 }
lib/ui/settings/widgets/scheduled_jobs_settings.dart (1)

38-52: Consider consolidating SyncResponseService instantiation.

Multiple SyncResponseService instances are created: in _loadPermittedJobs (line 40), as a field in _JobCardState (line 131), and inside _triggerJobSync (line 244). Consider using a single instance for consistency and to reduce object allocation overhead.

🔎 Proposed consolidation for _ScheduledJobsSettingsState
 class _ScheduledJobsSettingsState extends State<ScheduledJobsSettings> {
   List<String?> _permittedJobs = [];
   bool _isLoadingPermittedJobs = true;
+  final SyncResponseService _syncResponseService = SyncResponseService();

   @override
   void initState() {
     super.initState();
     _loadPermittedJobs();
   }

   Future<void> _loadPermittedJobs() async {
     try {
-      final service = SyncResponseService();
-      final permittedJobs = await service.getPermittedJobs();
+      final permittedJobs = await _syncResponseService.getPermittedJobs();

Also applies to: 130-131

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 096ac7c and 383f07a.

📒 Files selected for processing (12)
  • android/app/src/main/java/io/mosip/registration_client/HostApiModule.java
  • android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/config/AppModule.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAO.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/spi/LocalConfigService.java
  • lib/platform_android/sync_response_service_impl.dart
  • lib/platform_spi/sync_response_service.dart
  • lib/ui/settings/widgets/scheduled_jobs_settings.dart
  • pigeon/master_data_sync.dart
🧰 Additional context used
🧬 Code graph analysis (2)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/constant/RegistrationConstants.java (1)
  • RegistrationConstants (9-139)
android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/util/CronExpressionParser.java (1)
  • CronExpressionParser (19-65)
🔇 Additional comments (15)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAO.java (1)

29-34: LGTM!

The method signature and documentation are clear. The nullable return value is properly documented.

android/app/src/main/java/io/mosip/registration_client/HostApiModule.java (1)

187-214: LGTM!

The dependency injection wiring correctly adds LocalConfigService as a dependency to MasterDataSyncApi. The pattern is consistent with other provider methods in this module.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java (2)

38-41: LGTM!

Clean delegation to the DAO layer. The method signature matches the interface contract.


48-51: LGTM!

Proper use of the PERMITTED_JOB_TYPE constant from RegistrationConstants ensures consistency with the permitted configuration type system.

pigeon/master_data_sync.dart (1)

66-71: All three async API methods are properly implemented in MasterDataSyncApi.java.

Verification confirms that the Android implementation includes all three methods with correct signatures and proper async handling via the Pigeon Result callback pattern:

  • getPermittedJobs() — delegates to localConfigService.getPermittedJobs()
  • isValidCronExpression(String cronExpression) — validates using CronExpressionParser
  • modifyJobCronExpression(String jobId, String cronExpression) — delegates to localConfigService.modifyJob()

Each method includes appropriate exception handling and logging.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/spi/LocalConfigService.java (1)

26-44: LGTM! Well-documented interface additions.

The three new methods are clearly documented and follow consistent patterns with the existing interface:

  • getValue(String name): Clean read operation
  • modifyJob(String name, String value): Clear write operation for job configuration
  • getPermittedJobs(): Straightforward query for permitted job IDs

The method signatures and documentation are appropriate for the feature requirements.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/config/AppModule.java (1)

263-264: LGTM! Proper dependency injection setup.

The LocalConfigService dependency is correctly wired into JobManagerServiceImpl through Dagger. The singleton scope is maintained, and the parameter ordering is consistent with the constructor signature.

lib/platform_android/sync_response_service_impl.dart (1)

275-315: LGTM! Consistent error handling and implementation.

The three new platform bridge methods follow the established error handling patterns in this file:

  • Proper PlatformException handling with debug logging
  • Safe fallback values on error (empty list for getPermittedJobs, false for boolean methods)
  • Consistent structure with existing methods

The implementation correctly delegates to the platform API layer.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java (1)

191-204: LGTM! Robust override mechanism with proper fallback.

The getSyncFrequency helper correctly implements the local configuration override logic:

  • Null-safe check for localConfigService ensures graceful degradation
  • Trim validation prevents whitespace-only values from being used
  • Clean fallback to default syncJob.getSyncFreq() when no override exists

This enables per-job cron expression customization while maintaining backward compatibility.

android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java (3)

62-67: LGTM! Dependency injection for LocalConfigService is properly wired.

The new imports, field declaration, constructor parameter, and assignment for LocalConfigService follow the established pattern used for other services in this class.

Also applies to: 105-105, 126-126, 152-152


439-448: LGTM! Proper null handling and error propagation.

The method correctly handles null results by returning an empty list, and exception handling follows the established pattern in this class.


450-459: LGTM! Defensive error handling for cron validation.

The method correctly returns false on any exception rather than propagating an error, which is appropriate for a validation method. The outer try-catch is redundant given CronExpressionParser.isValidCronExpression already handles exceptions internally, but it provides an extra safety net.

lib/ui/settings/widgets/scheduled_jobs_settings.dart (3)

14-26: LGTM! Appropriate conversion to StatefulWidget.

The widget correctly extends StatefulWidget to manage the permitted jobs list and loading state. The createState() implementation follows Flutter conventions.


145-149: LGTM! Proper controller lifecycle management.

The TextEditingController is correctly initialized in initState and disposed in dispose, preventing memory leaks.


363-382: LGTM! Conditional UI for cron editing is well implemented.

The edit button only appears for permitted jobs, and the sync button is correctly hidden during editing mode to prevent conflicting user actions.

Also applies to: 386-386

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java (1)

99-116: The syncFreq parameter is not used in scheduling logic.

The scheduleJob method accepts a syncFreq parameter but ignores it entirely. The job is always scheduled with a fixed periodMillis = JOB_PERIODIC_SECONDS * 1000L (15 minutes). This means custom cron expressions from LocalConfigService won't actually affect the JobScheduler timing—only the "next sync time" display will reflect the custom cron.

If the intent is to honor custom cron expressions for scheduling, the syncFreq should influence periodMillis or use a different scheduling strategy.

🔎 Example fix to use cron-derived interval
 @Override
 public int scheduleJob(int jobId, String apiName, String syncFreq) {
     Class<?> clientJobService = getJobServiceImplClass(apiName);
     if (clientJobService == null) {
         throw new NotImplementedException("Job service : " + apiName + " not implemented");
     }

     ComponentName componentName = new ComponentName(context, clientJobService);
     JobInfo info;
-    //To schedule periodically
-    long periodMillis = JOB_PERIODIC_SECONDS * 1000L;
+    // Calculate period from cron expression if valid, otherwise use default
+    long periodMillis = JOB_PERIODIC_SECONDS * 1000L;
+    if (CronExpressionParser.isValidCronExpression(syncFreq)) {
+        Instant nextExecution = CronExpressionParser.getNextExecutionTime(syncFreq);
+        if (nextExecution != null) {
+            long delay = nextExecution.toEpochMilli() - System.currentTimeMillis();
+            if (delay > 0) {
+                periodMillis = delay;
+            }
+        }
+    }
     info = new JobInfo.Builder(jobId, componentName)
             .setRequiresCharging(false)
             .setPersisted(true)
             .setPeriodic(periodMillis)
             .build();
     return jobScheduler.schedule(info);
 }
♻️ Duplicate comments (1)
lib/ui/settings/widgets/scheduled_jobs_settings.dart (1)

226-230: Guard against null/empty job ID before calling modify.

As noted in a previous review, if widget.job.id is null or empty, an empty string is passed to modifyJobCronExpression, which could result in undefined behavior or a failed operation without clear feedback.

🧹 Nitpick comments (4)
android/app/src/main/java/io/mosip/registration_client/MainActivity.java (2)

282-286: Consider catching a specific exception type or revising the comment.

The try-catch block provides defensive unregistration, but it catches all exceptions which might hide unexpected issues. Since rescheduleReceiver is always registered in onCreate (line 237), it should always be safe to unregister.

Suggested improvements

Option 1: Catch specific exception

 try {
     unregisterReceiver(rescheduleReceiver);
-} catch (Exception e) {
-    // Receiver might not be registered, ignore
+} catch (IllegalArgumentException e) {
+    // Receiver was not registered (edge case), ignore
+    Log.d(getClass().getSimpleName(), "rescheduleReceiver was not registered", e);
 }

Option 2: Remove try-catch for consistency (if confident it's always registered)

 unregisterReceiver(broadcastReceiver);
-try {
-    unregisterReceiver(rescheduleReceiver);
-} catch (Exception e) {
-    // Receiver might not be registered, ignore
-}
+unregisterReceiver(rescheduleReceiver);

217-226: The implementation is correct – the RESCHEDULE_JOB broadcast is confirmed to be sent from MasterDataSyncApi when a cron expression is modified.

The rescheduleReceiver correctly handles the broadcast and triggers job rescheduling. The flow is working as intended: when cron expressions change, MasterDataSyncApi sends the broadcast, MainActivity receives it, and createBackgroundTask is called to reschedule AlarmManager-based jobs.

Consider using package-prefixed action constants for better maintainability and to avoid potential conflicts with other apps. Define broadcast actions as public static final constants (e.g., io.mosip.registration_client.RESCHEDULE_JOB) rather than plain string literals. This would also align with the existing pattern if similar constants are defined elsewhere in the codebase.

lib/ui/settings/widgets/scheduled_jobs_settings.dart (2)

145-157: Missing setState call after async cron expression load.

_loadCronExpression updates _cronController.text asynchronously but doesn't call setState. While TextEditingController updates will reflect in the TextField, this pattern is inconsistent with other async loaders in this class (_loadLastSyncTime, _loadNextSyncTime) that do call setState.

If there's any UI state dependent on whether the cron was loaded, or if you want to ensure a rebuild after load, consider wrapping the assignment in setState or at minimum calling setState(() {}) after updating the controller.

🔎 Optional fix for consistency
 Future<void> _loadCronExpression() async {
   if (widget.job.id != null && widget.job.id!.isNotEmpty) {
     // Check for custom cron expression first (matches desktop client logic)
     final customCron = await _syncResponseService.getValue(widget.job.id!);
-    if (customCron != null && customCron.trim().isNotEmpty) {
-      _cronController.text = customCron; // Use saved custom cron expression
-    } else {
-      _cronController.text = widget.job.syncFreq ?? ''; // Use default from DB
-    }
+    setState(() {
+      if (customCron != null && customCron.trim().isNotEmpty) {
+        _cronController.text = customCron; // Use saved custom cron expression
+      } else {
+        _cronController.text = widget.job.syncFreq ?? ''; // Use default from DB
+      }
+    });
   } else {
-    _cronController.text = widget.job.syncFreq ?? '';
+    setState(() {
+      _cronController.text = widget.job.syncFreq ?? '';
+    });
   }
 }

246-252: Consider implications of forced app restart.

Restart.restartApp() forcefully restarts the entire application. This could lead to:

  1. Loss of any unsaved work in other parts of the app
  2. Poor user experience if users are in the middle of another task

Consider alternative approaches such as:

  • Refreshing only the job scheduling components
  • Prompting the user before restart
  • Deferring restart until the user navigates away or closes the app
🔎 Proposed improvement: confirm before restart
         // Wait a moment for the user to see the message, then restart the app
-        await Future.delayed(const Duration(seconds: 2));
-        
-        // Restart the app to apply cron expression changes
-        if (mounted) {
-          Restart.restartApp();
-        }
+        if (mounted) {
+          final shouldRestart = await showDialog<bool>(
+            context: context,
+            builder: (ctx) => AlertDialog(
+              title: const Text('Restart Required'),
+              content: const Text('The app needs to restart to apply cron expression changes. Restart now?'),
+              actions: [
+                TextButton(
+                  onPressed: () => Navigator.of(ctx).pop(false),
+                  child: const Text('Later'),
+                ),
+                TextButton(
+                  onPressed: () => Navigator.of(ctx).pop(true),
+                  child: const Text('Restart'),
+                ),
+              ],
+            ),
+          );
+          if (shouldRestart == true) {
+            Restart.restartApp();
+          }
+        }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 383f07a and 302035f.

📒 Files selected for processing (8)
  • android/app/src/main/java/io/mosip/registration_client/MainActivity.java
  • android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java
  • android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java
  • lib/platform_android/sync_response_service_impl.dart
  • lib/platform_spi/sync_response_service.dart
  • lib/ui/settings/widgets/scheduled_jobs_settings.dart
  • pigeon/master_data_sync.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/platform_android/sync_response_service_impl.dart
🧰 Additional context used
🧬 Code graph analysis (2)
android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java (2)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/util/CronExpressionParser.java (1)
  • CronExpressionParser (19-65)
android/app/src/main/java/io/mosip/registration_client/UploadBackgroundService.java (1)
  • UploadBackgroundService (20-86)
android/app/src/main/java/io/mosip/registration_client/MainActivity.java (1)
android/app/src/main/java/io/mosip/registration_client/UploadBackgroundService.java (1)
  • UploadBackgroundService (20-86)
🔇 Additional comments (11)
android/app/src/main/java/io/mosip/registration_client/MainActivity.java (1)

234-237: LGTM!

The receiver registration is correct and the comment clearly explains its purpose.

android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java (4)

439-448: LGTM!

The getPermittedJobs implementation correctly handles null by returning an empty list, and exceptions are properly logged and propagated.


450-459: LGTM!

The isValidCronExpression implementation safely delegates to CronExpressionParser.isValidCronExpression and returns false on any exception, which is appropriate for a validation method.


498-507: LGTM!

The getValue implementation properly delegates to LocalConfigService with appropriate error handling and logging.


481-489: No action needed—the "RESCHEDULE_JOB" broadcast receiver is properly registered in MainActivity.java (lines 236-237) with a corresponding BroadcastReceiver implementation (lines 217-226) that extracts the job API name and calls createBackgroundTask() to reschedule. The broadcast will be received and handled correctly.

Likely an incorrect or invalid review comment.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java (1)

193-206: LGTM!

The getSyncFrequency helper method correctly prioritizes local configuration overrides while safely falling back to the default sync frequency. The null and empty string checks are appropriate.

android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java (1)

213-226: LGTM!

The custom cron expression lookup follows the same pattern as JobManagerServiceImpl.getSyncFrequency(), ensuring consistent behavior across scheduling components. The debug logging is helpful for troubleshooting which cron source is being used.

pigeon/master_data_sync.dart (1)

66-73: LGTM!

The new Pigeon API methods are well-defined with appropriate return types and align with the corresponding Android implementations in MasterDataSyncApi.

lib/platform_spi/sync_response_service.dart (1)

35-38: LGTM!

The abstract method signatures correctly mirror the Pigeon API definitions and provide the appropriate interface for the platform implementation.

lib/ui/settings/widgets/scheduled_jobs_settings.dart (2)

29-53: LGTM on permitted jobs loading.

The state management for loading permitted jobs is well-structured with proper error handling and loading state tracking.


347-400: LGTM on conditional cron editing UI.

The UI correctly renders an editable text field with validation for permitted jobs and falls back to a static display for non-permitted jobs. The error styling and submit button are appropriately implemented.

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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 (3)
lib/provider/sync_provider.dart (2)

55-56: Returning mutable internal map exposes state to external modification.

The getter returns the internal _jobStatuses map directly, allowing callers to mutate provider state without triggering notifyListeners().

🔎 Proposed fix
-  Map<String, JobStatus> get jobStatuses => _jobStatuses;
+  Map<String, JobStatus> get jobStatuses => Map.unmodifiable(_jobStatuses);

303-326: Sequential async calls in loop may slow polling with many jobs.

Each job iteration awaits getLastSyncTimeByJobId and getNextSyncTimeByJobId sequentially. With N jobs, this results in 2N serial async operations.

Consider parallelizing per-job fetches if job count grows or latency becomes noticeable.

🔎 Proposed parallel approach
   Future<void> refreshJobStatuses() async {
     try {
       final activeJobs = await syncResponseService.getActiveSyncJobs();
+      final futures = <Future<void>>[];
       for (final jobJson in activeJobs) {
-         if (jobJson == null) continue;
-         try {
-           final job = SyncJobDef.fromJson(json.decode(jobJson) as Map<String, dynamic>);
-           if (job.id != null) {
-              final lastSync = await getLastSyncTimeByJobId(job.id!);
-              final nextSync = await getNextSyncTimeByJobId(job.id!);
-              
-              _jobStatuses[job.id!] = JobStatus(id: job.id!, lastSyncTime: lastSync, nextSyncTime: nextSync);
-           }
-         } catch (e) {
-           log("Error parsing job during polling: $e");
-         }
+        if (jobJson == null) continue;
+        futures.add(_fetchAndUpdateJobStatus(jobJson));
       }
+      await Future.wait(futures);

       await getLastSyncTime(); // Update global last sync time (fallback for Master Sync)
       notifyListeners();
     } catch (e) {
       log("Error refreshing job statuses: $e");
     }
   }
+
+  Future<void> _fetchAndUpdateJobStatus(String jobJson) async {
+    try {
+      final job = SyncJobDef.fromJson(json.decode(jobJson) as Map<String, dynamic>);
+      if (job.id != null) {
+        final results = await Future.wait([
+          getLastSyncTimeByJobId(job.id!),
+          getNextSyncTimeByJobId(job.id!),
+        ]);
+        _jobStatuses[job.id!] = JobStatus(
+          id: job.id!,
+          lastSyncTime: results[0],
+          nextSyncTime: results[1],
+        );
+      }
+    } catch (e) {
+      log("Error parsing job during polling: $e");
+    }
+  }
lib/ui/settings/widgets/scheduled_jobs_settings.dart (1)

59-74: Store provider reference to avoid context access in deactivate.

The comments in dispose() reveal uncertainty about lifecycle handling. Accessing context.read in deactivate() works but is fragile. Store the provider reference in initState for safer cleanup.

🔎 Proposed fix
 class _ScheduledJobsSettingsState extends State<ScheduledJobsSettings> {
   List<String?> _permittedJobs = [];
   bool _isLoadingPermittedJobs = true;
+  late SyncProvider _syncProvider;

   @override
   void initState() {
     super.initState();
+    _syncProvider = context.read<SyncProvider>();
     WidgetsBinding.instance.addPostFrameCallback((_) {
-      context.read<SyncProvider>().startJobPolling();
+      _syncProvider.startJobPolling();
     });
     _loadPermittedJobs();
   }

   @override
   void dispose() {
-    // Only stop polling if we are sure we started it and valid context access
-    // But since context might be invalid, we skip explicitly stopping here 
-    // unless we store the provider reference. 
-    // Actually, let's store it in initState or just accept it runs? 
-    // Best practice: Access provider in didChangeDependencies or similar?
-    // For now, let's leave start in initState and we can add a deactivate hook.
+    _syncProvider.stopJobPolling();
     super.dispose();
   }
-  
-  @override
-  void deactivate() {
-     context.read<SyncProvider>().stopJobPolling();
-     super.deactivate();
-  }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 302035f and 89bd607.

📒 Files selected for processing (2)
  • lib/provider/sync_provider.dart
  • lib/ui/settings/widgets/scheduled_jobs_settings.dart
🔇 Additional comments (6)
lib/provider/sync_provider.dart (2)

290-301: LGTM!

Timer lifecycle is correctly managed: startJobPolling cancels any existing timer before creating a new one, and dispose properly stops polling.

Also applies to: 328-332


335-341: LGTM!

Simple immutable data class with appropriate structure for job status tracking.

lib/ui/settings/widgets/scheduled_jobs_settings.dart (4)

240-248: App restart is disruptive UX—verify if necessary.

Restarting the entire app after a cron change can cause loss of unsaved user state and is jarring. Confirm whether the platform layer truly requires an app restart to apply cron expression changes, or if jobs can be rescheduled dynamically.


339-355: LGTM!

Good use of Consumer<SyncProvider> to reactively display job statuses with appropriate fallback handling for the masterSyncJob case.


356-409: LGTM!

Cron editing UI is properly gated behind isPermitted check with inline validation feedback.


158-170: LGTM!

Correct precedence for loading cron expression: custom value first, then default from job definition.

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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: 4

🤖 Fix all issues with AI Agents
In @lib/ui/settings/widgets/scheduled_jobs_settings.dart:
- Around line 158-170: The _loadCronExpression method can throw from
_syncResponseService.getValue and leave _cronController.text stale; wrap the
getValue call in a try-catch, set a fallback (_cronController.text =
widget.job.syncFreq ?? '') in the catch and also ensure the controller is
assigned in the non-exception path (after the await), and log or handle the
caught exception; update references in _loadCronExpression,
_syncResponseService.getValue, and _cronController accordingly.
- Around line 60-74: The fix: avoid calling context.read<SyncProvider>() in
deactivate; instead obtain and cache the provider reference in
didChangeDependencies (e.g. assign to a private field like _syncProvider of type
SyncProvider or ProviderListener) and then call _syncProvider.stopJobPolling()
from dispose where context is safe to access during teardown; remove or replace
the context.read call in deactivate (no direct context access there) and ensure
the cached field is nullable/checked if necessary before calling stopJobPolling
to avoid exceptions.
- Around line 357-359: The code sets lastSync via
formatDate(provider.lastSuccessfulSyncTime) when widget.job.apiName ==
"masterSyncJob" but doesn't check for an empty lastSuccessfulSyncTime, which
yields a blank display; update the branch in the scheduled jobs widget to first
check provider.lastSuccessfulSyncTime (or .isNotEmpty) and only call formatDate
if present, otherwise assign a fallback string like "Never synced" to lastSync
so the UI shows a meaningful message; change the logic around
widget.job.apiName, lastSync, provider.lastSuccessfulSyncTime and formatDate
accordingly.
- Around line 256-261: The Restart.restartApp() call will fail on iOS because
the restart_app package requires a CFBundleURLTypes entry in Info.plist; update
the iOS app’s Info.plist to add a CFBundleURLTypes array with a dictionary
including a CFBundleURLName and CFBundleURLSchemes (a unique scheme for the app)
so the package can trigger the restart, then rebuild the iOS app and verify
Restart.restartApp() successfully restarts the app on iOS.
🧹 Nitpick comments (2)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)

126-131: Consider removing unused parameter.

The configType parameter is passed but never used within the method body. This reduces code clarity.

🔎 Proposed refactor
-private void updateLocalPreference(LocalPreferences localPreference, String configType) {
+private void updateLocalPreference(LocalPreferences localPreference) {
     localPreference.setIsDeleted(true);
     localPreference.setUpdBy(RegistrationConstants.JOB_TRIGGER_POINT_USER);
     localPreference.setUpdDtimes(System.currentTimeMillis());
     localPreferencesRepository.save(localPreference);
 }

And update the call site at line 110:

-updateLocalPreference(localPreferences, RegistrationConstants.PERMITTED_JOB_TYPE);
+updateLocalPreference(localPreferences);
lib/ui/settings/widgets/scheduled_jobs_settings.dart (1)

381-396: Consider using errorText instead of custom errorBorder logic.

Setting errorText: null while conditionally applying errorBorder based on _cronError is inconsistent. The built-in errorText parameter would simplify the decoration and provide consistent Material Design styling.

🔎 Proposed refactor
                                 child: TextField(
                                   controller: _cronController,
                                   decoration: InputDecoration(
                                     hintText: 'Cron Expression',
-                                    errorText: null,
-                                    errorBorder: _cronError != null 
-                                        ? const OutlineInputBorder(
-                                            borderSide: BorderSide(color: Colors.red, width: 1))
-                                        : null,
                                     border: const OutlineInputBorder(),
                                     isDense: true,
                                     contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
                                   ),
                                   style: const TextStyle(fontSize: 11),
                                 ),
                               ),
-                              if (_cronError != null)
-                                Padding(
-                                  padding: const EdgeInsets.only(top: 1.0, left: 4.0),
-                                  child: Text(
-                                    _cronError!,
-                                    style: const TextStyle(fontSize: 9, color: Colors.red, height: 1),
-                                  ),
-                                ),

If you prefer keeping the custom error display, at least remove the errorText: null line as it's redundant.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89bd607 and ee50c75.

📒 Files selected for processing (5)
  • android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/config/RoomModule.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java
  • lib/ui/settings/widgets/scheduled_jobs_settings.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java
🧰 Additional context used
🧬 Code graph analysis (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/util/CronExpressionParser.java (1)
  • CronExpressionParser (19-65)
🔇 Additional comments (8)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/config/RoomModule.java (1)

472-478: LGTM! Proper dependency injection for transactional support.

The addition of ClientDatabase parameter enables the DAO layer to perform atomic transaction operations (via runInTransaction()), which is essential for ensuring data consistency in job modification operations.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java (3)

43-45: LGTM! Clean delegation to DAO layer.

The getValue method appropriately delegates to the DAO layer for data retrieval.


48-65: Excellent validation before persistence!

The modifyJob method implements proper defensive validation:

  • Rejects null/empty values early
  • Validates cron expression syntax using CronExpressionParser
  • Provides clear error messages for debugging
  • Only delegates to DAO after validation passes

This prevents invalid cron expressions from being persisted, which would cause job scheduling failures at runtime.


68-70: LGTM! Clean delegation for permitted jobs retrieval.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (3)

26-26: LGTM! ClientDatabase injection enables transactional operations.

The ClientDatabase field is properly injected and stored, enabling atomic transaction support for job modifications via runInTransaction().

Also applies to: 31-31, 34-34


85-95: LGTM! Clean value retrieval with error handling.

The getValue method safely retrieves configuration values with null checks and error logging.


98-121: Excellent atomic transaction handling!

The modifyJob method properly wraps the soft-delete and create operations in a single transaction using clientDatabase.runInTransaction(). This ensures:

  • Atomicity: Both operations succeed or both roll back
  • Thread safety: Transaction boundaries prevent race conditions
  • Data consistency: No partial updates can occur

The exception handling correctly re-throws to trigger rollback on failure.

lib/ui/settings/widgets/scheduled_jobs_settings.dart (1)

220-232: Good defensive validation for job ID.

The null/empty check for job ID before calling modifyJobCronExpression properly addresses the previous review feedback and prevents undefined behavior.

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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: 2

🤖 Fix all issues with AI Agents
In
@android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java:
- Around line 121-129: The method updateLocalPreference(LocalPreferences
localPreference, String configType) contains an unused parameter configType;
remove the unused parameter from the method signature in LocalConfigDAOImpl
(change to updateLocalPreference(LocalPreferences localPreference)), update any
internal references if present, and then update all call sites (e.g., the caller
around the previous call at line ~108) to invoke updateLocalPreference with only
the LocalPreferences argument; keep the existing body (setting isDeleted, updBy,
updDtimes, and saving via localPreferencesRepository) unchanged.

In @lib/ui/settings/widgets/scheduled_jobs_settings.dart:
- Around line 318-319: The async callback in the ScheduledJobsSettings widget
calls context.read<SyncProvider>().refreshJobStatuses() after an await without
verifying widget lifecycle; add a mounted check before accessing context by
returning early if (!mounted) or wrapping the context.read call in if (mounted)
{ context.read<SyncProvider>().refreshJobStatuses(); } so the call only runs
when the State is still mounted.
🧹 Nitpick comments (7)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)

84-95: Consider distinguishing between "not found" and error cases.

The method returns null for both scenarios where the preference doesn't exist and when an exception occurs. Callers cannot differentiate between these cases, which might complicate error handling upstream.

If the distinction matters, consider either throwing the exception or returning an Optional<String> with more specific error signaling.

lib/ui/settings/widgets/scheduled_jobs_settings.dart (6)

175-187: Consider returning a more user-friendly fallback.

The formatDate method returns the original dateString on error, which might display a malformed or raw timestamp in the UI. Consider returning a placeholder like "Invalid date" or "-" for better UX.

🔎 Optional improvement
     } catch(e) {
-      return dateString;
+      return "-";
     }

252-258: Add loading indicator during async cron modification.

The 2-second delay before restart happens without a loading indicator, which could confuse users. Additionally, if the user navigates away during this period, the app restart might be unexpected.

Consider:

  1. Showing a loading dialog during the save/restart sequence
  2. Disabling the Submit button and showing a progress indicator
  3. Preventing navigation during the critical period
🔎 Proposed improvement with loading dialog
       if (success && mounted) {
         // Clear error
         setState(() {
           _cronError = null;
         });
         
-        // Show success message
-        ScaffoldMessenger.of(context).showSnackBar(
-          const SnackBar(
-            content: Text('Cron expression saved successfully. Restarting app...'),
-            duration: Duration(seconds: 2),
-          ),
-        );
-        
-        // Wait a moment for the user to see the message, then restart the app
-        await Future.delayed(const Duration(seconds: 2));
+        // Show loading dialog
+        if (mounted) {
+          showDialog(
+            context: context,
+            barrierDismissible: false,
+            builder: (context) => const AlertDialog(
+              content: Row(
+                children: [
+                  CircularProgressIndicator(),
+                  SizedBox(width: 16),
+                  Text('Restarting app...'),
+                ],
+              ),
+            ),
+          );
+        }
+        
+        // Brief delay to show the dialog
+        await Future.delayed(const Duration(milliseconds: 500));
         
         // Restart the app to apply cron expression changes
         if (mounted) {
           Restart.restartApp();
         }

409-416: Disable Submit button during async operation.

The Submit button remains enabled while _modifyCronExpression is executing, allowing users to tap it multiple times and trigger concurrent save operations. This could lead to race conditions or duplicate API calls.

🔎 Proposed fix with loading state

Add a loading state field:

 class _JobCardState extends State<_JobCard> {
   String? _lastSync;
   String? _nextSync;
   late SyncProvider syncProvider;
   final TextEditingController _cronController = TextEditingController();
   final SyncResponseService _syncResponseService = SyncResponseService();
   String? _cronError;
+  bool _isSaving = false;

Then wrap the button logic:

                         SizedBox(
                           height: 32,
                           width: 65,
                           child: ElevatedButton(
-                            onPressed: _modifyCronExpression,
+                            onPressed: _isSaving ? null : _modifyCronExpression,
                             style: ElevatedButton.styleFrom(
                               padding: EdgeInsets.zero,
                             ),
-                            child: const Text('Submit', style: TextStyle(fontSize: 11)),
+                            child: _isSaving 
+                                ? const SizedBox(
+                                    width: 16, 
+                                    height: 16, 
+                                    child: CircularProgressIndicator(strokeWidth: 2),
+                                  )
+                                : const Text('Submit', style: TextStyle(fontSize: 11)),
                           ),
                         ),

And update _modifyCronExpression:

   Future<void> _modifyCronExpression() async {
+    setState(() {
+      _isSaving = true;
+    });
+    
     final cronExpression = _cronController.text.trim();
     
     if (cronExpression.isEmpty) {
       setState(() {
         _cronError = 'Cron expression cannot be empty';
+        _isSaving = false;
       });
       return;
     }

(Apply similar pattern to all early returns and the finally block)


30-30: Consider using Set for _permittedJobs to improve lookup performance.

The code uses _permittedJobs.contains(job.id) for permission checks, which has O(n) complexity on a List. If the number of permitted jobs grows, consider using a Set for O(1) lookups.

🔎 Proposed optimization
 class _ScheduledJobsSettingsState extends State<ScheduledJobsSettings> {
-  List<String?> _permittedJobs = [];
+  Set<String?> _permittedJobs = {};
   bool _isLoadingPermittedJobs = true;
   SyncProvider? _syncProvider;

Then in _loadPermittedJobs:

       setState(() {
-        _permittedJobs = permittedJobs;
+        _permittedJobs = permittedJobs.toSet();
         _isLoadingPermittedJobs = false;
       });

Also applies to: 109-109


116-116: Extract magic number for bottom padding.

The bottom padding calculation includes a magic number 230 which makes the intent unclear. Consider extracting this as a named constant for better maintainability.

🔎 Proposed improvement
+// Extra padding to account for expanded job cards
+const double _kJobCardBottomPadding = 230.0;
+
 class ScheduledJobsSettings extends StatefulWidget {
   ...
 }

...

-          SliverToBoxAdapter(child: SizedBox(height: MediaQuery.of(context).padding.bottom + kBottomNavigationBarHeight + 230)),
+          SliverToBoxAdapter(child: SizedBox(height: MediaQuery.of(context).padding.bottom + kBottomNavigationBarHeight + _kJobCardBottomPadding)),

189-272: Consider extracting validation and save logic to separate methods.

The _modifyCronExpression method handles validation, saving, user feedback, and app restart in a single function. Consider extracting validation and save operations into separate methods for better testability and maintainability.

🔎 Example refactoring
Future<bool> _validateCronExpression(String cronExpression) async {
  if (cronExpression.isEmpty) {
    setState(() {
      _cronError = 'Cron expression cannot be empty';
    });
    return false;
  }

  final isValid = await _syncResponseService.isValidCronExpression(cronExpression);
  if (!isValid) {
    setState(() {
      _cronError = 'Invalid cron expression';
    });
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Invalid cron expression')),
      );
    }
    return false;
  }

  setState(() {
    _cronError = null;
  });
  return true;
}

Future<bool> _saveCronExpression(String jobId, String cronExpression) async {
  try {
    return await _syncResponseService.modifyJobCronExpression(
      jobId,
      cronExpression,
    );
  } catch (e) {
    debugPrint('Error modifying cron expression: $e');
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Error: $e')),
      );
    }
    return false;
  }
}

Future<void> _modifyCronExpression() async {
  final cronExpression = _cronController.text.trim();
  
  // Validate expression
  if (!await _validateCronExpression(cronExpression)) {
    return;
  }

  // Validate job ID
  final jobId = widget.job.id;
  if (jobId == null || jobId.isEmpty) {
    setState(() {
      _cronError = 'Job ID is required';
    });
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Cannot save cron expression: Job ID is missing')),
      );
    }
    return;
  }

  // Save and restart
  final success = await _saveCronExpression(jobId, cronExpression);
  if (success && mounted) {
    // Show success and restart
    // ... (existing restart logic)
  }
}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee50c75 and 9cf2645.

📒 Files selected for processing (2)
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java
  • lib/ui/settings/widgets/scheduled_jobs_settings.dart
🧰 Additional context used
🧬 Code graph analysis (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/constant/RegistrationConstants.java (1)
  • RegistrationConstants (9-139)
🔇 Additional comments (6)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (2)

13-13: LGTM: Dependency injection properly implemented.

The ClientDatabase dependency is correctly injected via constructor and stored as a private field, following Dagger DI best practices with @Inject and @Singleton annotations.

Also applies to: 26-26, 30-34


97-119: LGTM: Transaction handling correctly implemented.

The method now wraps both the soft-delete and create operations in clientDatabase.runInTransaction(), ensuring atomicity and addressing the previous review concern. The re-throw pattern correctly triggers transaction rollback on failure.

lib/ui/settings/widgets/scheduled_jobs_settings.dart (4)

149-167: LGTM - Error handling properly implemented.

The _loadCronExpression method now correctly handles errors from getValue with a try-catch block and falls back to the default cron expression. This addresses the previous review concern.


350-366: Dynamic job status display looks good.

The Consumer<SyncProvider> properly rebuilds when job statuses change, and the special handling for masterSyncJob correctly uses the global lastSuccessfulSyncTime. The formatDate method handles empty or invalid values gracefully.


443-453: LGTM - Clean key-value display helper.

The _kv helper method is well-structured with proper overflow handling via Flexible. The styling is consistent and appropriate for the UI.


37-40: The startJobPolling() implementation already includes duplicate-start protection by cancelling any existing timer before creating a new one (line 292 in lib/provider/sync_provider.dart). No changes needed.

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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: 1

🧹 Nitpick comments (1)
lib/ui/settings/widgets/scheduled_jobs_settings.dart (1)

377-394: Consider clearing validation errors as user types.

The TextField displays validation errors via _cronError, but errors persist until the user submits again. Adding an onChanged callback to clear _cronError when the user starts editing would provide better immediate feedback.

🔎 Proposed enhancement
                                 TextField(
                                   controller: _cronController,
+                                  onChanged: (value) {
+                                    if (_cronError != null) {
+                                      setState(() {
+                                        _cronError = null;
+                                      });
+                                    }
+                                  },
                                   decoration: InputDecoration(
                                     hintText: 'Cron Expression',
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9cf2645 and 5dce80a.

📒 Files selected for processing (2)
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java
  • lib/ui/settings/widgets/scheduled_jobs_settings.dart
🧰 Additional context used
🧬 Code graph analysis (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/constant/RegistrationConstants.java (1)
  • RegistrationConstants (9-139)
🔇 Additional comments (7)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (2)

26-35: LGTM!

Clean dependency injection of ClientDatabase to enable transactional operations. The constructor properly initializes all fields.


84-95: LGTM!

The method has proper null safety with checks on both the entity and its value. Error handling with logging and null fallback is appropriate for a retrieval method.

lib/ui/settings/widgets/scheduled_jobs_settings.dart (5)

29-65: Well-structured state management and lifecycle handling.

The implementation correctly:

  • Uses addPostFrameCallback to safely access context during initialization
  • Stores provider reference to avoid context access issues in dispose
  • Includes mounted checks before setState in async operations
  • Handles errors gracefully in _loadPermittedJobs

This addresses previous concerns about context access in deactivate and ensures clean polling lifecycle management.


149-167: Good error handling for cron expression loading.

The _loadCronExpression method properly:

  • Wraps async operations in try-catch
  • Provides fallback to default cron expression on error
  • Validates job ID before making service calls
  • Logs errors for debugging

This addresses the previous concern about unhandled exceptions from getValue.


217-229: Proper validation of job ID before save operation.

The validation correctly checks for null or empty job ID before calling modifyJobCronExpression, providing clear user feedback via SnackBar and preventing invalid operations. This addresses the previous concern about passing empty IDs to the service.


318-320: Proper mounted check before context access.

The mounted check before calling context.read<SyncProvider>() prevents exceptions if the widget is disposed during the async sync operation. Good defensive programming.


425-437: Manual sync button does not need isPermitted check — this is correct by design.

The isPermitted flag controls permission to edit cron schedules (lines 368-407), not to trigger manual sync operations. These are separate permissions: manual sync (triggered by the button at lines 425-437) is available for all non-packet jobs, while cron editing is restricted to permitted jobs. This design is consistent with the permission model used elsewhere in the codebase (e.g., global config editing).

Likely an incorrect or invalid review comment.

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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)
lib/ui/settings/widgets/scheduled_jobs_settings.dart (2)

44-58: Consider showing user feedback when permitted jobs fail to load.

If getPermittedJobs() throws an exception, the error is logged but the UI shows an empty permitted jobs list without any indication to the user that loading failed. Consider displaying a SnackBar or error message so users understand why no jobs appear as editable.

🔎 Proposed fix
   Future<void> _loadPermittedJobs() async {
     try {
       final service = SyncResponseService();
       final permittedJobs = await service.getPermittedJobs();
       if (!mounted) return;
       setState(() {
         _permittedJobs = permittedJobs;
         _isLoadingPermittedJobs = false;
       });
     } catch (e) {
       debugPrint('Failed to load permitted jobs: $e');
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('Failed to load job permissions: $e')),
+        );
+      }
       setState(() {
         _isLoadingPermittedJobs = false;
       });
     }
   }

150-168: Consider showing user feedback when cron expression fails to load.

While the try-catch properly handles errors and falls back to the default cron expression, users are not informed when loading fails. This could lead to confusion if the custom cron expression exists but fails to load, causing the UI to display the default value instead.

🔎 Proposed fix
   Future<void> _loadCronExpression() async {
     try {
       if (widget.job.id != null && widget.job.id!.isNotEmpty) {
         final customCron = await _syncResponseService.getValue(widget.job.id!);
         if (customCron != null && customCron.trim().isNotEmpty) {
           _cronController.text = customCron;
         } else {
           _cronController.text = widget.job.syncFreq ?? '';
         }
       } else {
         _cronController.text = widget.job.syncFreq ?? '';
       }
     } catch (e) {
       debugPrint('Failed to load cron expression: $e');
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('Failed to load custom cron expression: $e')),
+        );
+      }
       _cronController.text = widget.job.syncFreq ?? '';
     }
   }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5dce80a and f3470c1.

📒 Files selected for processing (1)
  • lib/ui/settings/widgets/scheduled_jobs_settings.dart

sachin.sp added 2 commits January 8, 2026 16:54
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/SyncJobDefDao.java (1)

51-51: Fix operator precedence in the existing query.

Line 51 has the same SQL operator precedence issue that was fixed in Line 34. The current query where is_deleted == 0 or is_deleted is null and is_active=:isActive will be parsed as (is_deleted == 0) or (is_deleted is null and is_active=:isActive), which may return deleted records.

🔒 Proposed fix
-    @Query("select * from sync_job_def where is_deleted == 0 or is_deleted is null and is_active=:isActive")
+    @Query("select * from sync_job_def where (is_deleted == 0 or is_deleted is null) and is_active=:isActive")
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3470c1 and 9792426.

📒 Files selected for processing (6)
  • android/app/src/main/java/io/mosip/registration_client/HostApiModule.java
  • android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java
  • android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/SyncJobDefDao.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/SyncJobDefRepository.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/app/src/main/java/io/mosip/registration_client/HostApiModule.java
🧰 Additional context used
🧬 Code graph analysis (1)
android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java (2)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/util/CronExpressionParser.java (1)
  • CronExpressionParser (19-65)
android/app/src/main/java/io/mosip/registration_client/UploadBackgroundService.java (1)
  • UploadBackgroundService (20-86)
🔇 Additional comments (14)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/SyncJobDefDao.java (2)

31-35: LGTM! Critical operator precedence fix.

The added parentheses correctly enforce the intended query logic: (is_deleted == 0 or is_deleted is null) and id=:jobId. Without parentheses, SQL would evaluate as is_deleted == 0 or (is_deleted is null and id=:jobId), potentially returning wrong results.


37-44: LGTM! Consistent implementation pattern.

The new findOneByApiName method correctly applies the same parenthesized condition pattern as the fixed findOneById query.

android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java (6)

62-62: LGTM! Proper dependency injection.

LocalConfigService is correctly integrated through constructor injection and field assignment following the existing DI pattern.

Also applies to: 105-105, 126-126, 152-152


439-448: LGTM! Safe null handling.

The method returns an empty list when permittedJobs is null, preventing potential NPEs in callers.


450-459: LGTM! Defensive exception handling.

The method gracefully catches validation errors and returns false, ensuring the API never throws to Pigeon callers.


491-500: LGTM! Straightforward delegation.

The method delegates to LocalConfigService with appropriate error handling.


586-596: LGTM! Improved error handling.

The refactored implementation now uses getSyncJobDefByApiName and properly checks for null before accessing the ID, avoiding potential NPEs.


461-489: The code is correct. MainActivity has a properly registered BroadcastReceiver for the "RESCHEDULE_JOB" action (lines 236-237 in MainActivity), and it is properly implemented (lines 217-226) to handle job rescheduling by calling createBackgroundTask() with the job API name extracted from the broadcast intent.

Likely an incorrect or invalid review comment.

android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java (2)

26-26: LGTM! Proper dependency injection.

LocalConfigService is correctly integrated following the existing constructor injection pattern.

Also applies to: 37-37, 42-49


207-228: LGTM! Clean custom cron override logic.

The implementation correctly:

  • Fetches the default cron from the database
  • Checks if localConfigService is available
  • Overrides with custom cron only if non-null and non-empty
  • Logs both default and custom cron expressions for debugging
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/SyncJobDefRepository.java (1)

40-58: LGTM! Clean repository accessors.

Both methods are straightforward DAO delegations with clear Javadoc documenting the null-return behavior when records are not found.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (3)

13-13: LGTM! Transactional support added.

ClientDatabase is correctly injected to enable transactional operations in the new modifyJob method.

Also applies to: 26-26, 29-35


84-95: LGTM! Safe value retrieval.

The method handles exceptions gracefully and returns null when the preference is not found or an error occurs.


97-122: LGTM! Proper transactional semantics.

The method correctly wraps the read-check-write sequence in clientDatabase.runInTransaction to ensure atomicity and prevent race conditions. Re-throwing as RuntimeException (Line 119) properly triggers transaction rollback on failure.

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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: 3

🤖 Fix all issues with AI agents
In
@android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java:
- Around line 93-112: The modifyJob method swallows all exceptions and lacks
input validation; change it to validate inputs (ensure name and value are
non-null/non-empty) at the start, avoid catching broad Exception (or rethrow
after logging) and instead propagate failure to callers by throwing a specific
runtime exception or returning a boolean status; update callers accordingly (or
document the thrown exception), and ensure saveLocalPreference and
localPreferencesRepository.save calls are executed only after validation so
failures are not silently logged in modifyJob.
- Around line 93-112: modifyJob duplicates the update/create logic used in
modifyConfigurations; extract the shared logic into a private helper (e.g.,
upsertLocalPreference or updateOrCreateLocalPreference) that accepts name, value
and configType, reuse it from both modifyJob and modifyConfigurations, and call
saveLocalPreference from within that helper with the appropriate
RegistrationConstants value; update modifyJob to call the helper instead of
repeating the block and keep exception logging as-is.
- Around line 93-112: LocalConfigServiceImpl.modifyJob currently validates cron
expressions but does not check whether the provided name is a permitted job;
before calling the DAO (or delegating to LocalConfigDAOImpl.modifyJob), call
getPermittedJobs() and if it does not contain name, log an error and throw
IllegalArgumentException (use the suggested message), so only jobs listed by
getPermittedJobs() can be modified.
🧹 Nitpick comments (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)

80-91: Consider distinguishing between "not found" and "error" cases.

The method returns null for both scenarios: when the preference doesn't exist and when an exception occurs. This makes it difficult for callers to distinguish between legitimate absence and an error condition. Additionally, there's no validation that name is non-null or non-empty before querying.

Consider either:

  • Using Optional to better express absence
  • Throwing an unchecked exception for errors while returning null only for "not found"
  • Adding input validation for the name parameter
♻️ Possible approaches

Approach 1: Add input validation

 @Override
 public String getValue(String name) {
+    if (name == null || name.trim().isEmpty()) {
+        Log.w(TAG, "getValue called with invalid name");
+        return null;
+    }
     try {
         LocalPreferences localPreference = localPreferencesRepository.findByIsDeletedFalseAndName(name);
         if (localPreference != null && localPreference.getVal() != null) {
             return localPreference.getVal();
         }
     } catch (Exception e) {
         Log.e(TAG, "Error getting value for: " + name, e);
+        throw new RuntimeException("Failed to retrieve configuration: " + name, e);
     }
     return null;
 }

Approach 2: Use Optional (if project supports Java 8+)

+import java.util.Optional;
+
 @Override
-public String getValue(String name) {
+public Optional<String> getValue(String name) {
     try {
         LocalPreferences localPreference = localPreferencesRepository.findByIsDeletedFalseAndName(name);
-        if (localPreference != null && localPreference.getVal() != null) {
-            return localPreference.getVal();
-        }
+        return Optional.ofNullable(localPreference)
+            .map(LocalPreferences::getVal);
     } catch (Exception e) {
         Log.e(TAG, "Error getting value for: " + name, e);
+        throw new RuntimeException("Failed to retrieve configuration: " + name, e);
     }
-    return null;
+    return Optional.empty();
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9792426 and e0ace6a.

📒 Files selected for processing (1)
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java
🧰 Additional context used
🧬 Code graph analysis (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/constant/RegistrationConstants.java (1)
  • RegistrationConstants (9-144)

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)

135-163: cleanUpLocalPreferences() can NPE on null permitted configs and null active flags.

  • permittedConfigs is not null-checked before the for loop on Line 142.
  • config.getIsActive() can be null; then !permittedStatusMap.get(key) on Line 154 can throw due to Boolean unboxing.
Proposed fix (null-safe)
 public void cleanUpLocalPreferences() {
     List<PermittedLocalConfig> permittedConfigs = permittedLocalConfigRepository
             .getPermittedConfigsByType(RegistrationConstants.PERMITTED_CONFIG_TYPE);

     Map<String, String> localConfigs = getLocalConfigurations();

     Map<String, Boolean> permittedStatusMap = new java.util.HashMap<>();
-    for (PermittedLocalConfig config : permittedConfigs) {
-        permittedStatusMap.put(config.getName(), config.getIsActive());
-    }
+    if (permittedConfigs != null) {
+        for (PermittedLocalConfig config : permittedConfigs) {
+            permittedStatusMap.put(config.getName(), Boolean.TRUE.equals(config.getIsActive()));
+        }
+    }

-    for (String key : localConfigs.keySet()) {
+    if (localConfigs == null || localConfigs.isEmpty()) return;
+    for (String key : localConfigs.keySet()) {
         LocalPreferences pref = localPreferencesRepository.findByIsDeletedFalseAndName(key);
         if (pref == null)
             continue;

         if (!permittedStatusMap.containsKey(key)) {
             localPreferencesRepository.delete(pref);
             Log.i(TAG, "Local preference deleted (row removed): " + key);
-        } else if (!permittedStatusMap.get(key)) {
+        } else if (Boolean.FALSE.equals(permittedStatusMap.get(key))) {
             // Key deactivated, mark as deleted
             pref.setIsDeleted(true);
             pref.setUpdBy(RegistrationConstants.JOB_TRIGGER_POINT_USER);
             pref.setUpdDtimes(System.currentTimeMillis());
             localPreferencesRepository.save(pref);
             Log.i(TAG, "Local preference marked deleted (row deactivated): " + key);
         }
     }
 }
🤖 Fix all issues with AI agents
In
@android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java:
- Around line 80-95: The DAO is vulnerable to cross-contamination because
lookups ignore configType; add a repository/DAO method
findByIsDeletedFalseAndNameAndConfigType(String name, String configType) and use
it in LocalConfigDAOImpl: update saveOrUpdateLocalPreference(...) to call
findByIsDeletedFalseAndNameAndConfigType(name, configType) instead of
findByIsDeletedFalseAndName(name); similarly change getValue(...) to use
findByIsDeletedFalseAndNameAndConfigType(name,
RegistrationConstants.PERMITTED_JOB_TYPE) (or appropriate configType) and update
cleanUpLocalPreferences(...) to filter by configType via the new
findByIsDeletedFalseAndNameAndConfigType method so JOB and CONFIGURATION records
are looked up/modified separately.

In
@android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java:
- Around line 47-70: modifyJob currently only validates the cron value; first
add explicit validation for the job name (null or blank) at the start of
modifyJob(String name, String value) — if name is null or empty, Log.e with a
clear message and throw an IllegalArgumentException like "Job name cannot be
null or empty" before any permitted-job checks so callers get the correct
failure reason; keep the existing cron validation and permitted-job check order
after the name validation and only call localConfigDAO.modifyJob(name, value) if
all validations pass.
🧹 Nitpick comments (3)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (2)

53-65: Avoid persisting non-permitted keys in modifyConfigurations() (or document the contract).

On Line 55–64 you now upsert every entry via saveOrUpdateLocalPreference(...) without checking that name is in the permitted CONFIGURATION list. If upstream isn’t already enforcing this, you can end up storing keys that will later be deleted/marked-deleted by cleanup (and create surprising UX).

Proposed adjustment (validate permitted keys once)
 public void modifyConfigurations(Map<String, String> localPreferences) {

+    List<String> permitted = getPermittedConfigurations(RegistrationConstants.PERMITTED_CONFIG_TYPE);
     for (Map.Entry<String, String> entry : localPreferences.entrySet()) {
         String name = entry.getKey();
         String value = entry.getValue();

         try {
+            if (permitted.contains(name)) {
                 saveOrUpdateLocalPreference(name, value, RegistrationConstants.PERMITTED_CONFIG_TYPE);
+            } else {
+                Log.w(TAG, "Ignoring non-permitted configuration key: " + name);
+            }
         } catch (Exception e) {
             Log.e(TAG, "Error modifying configuration: " + name, e);
         }
     }
 }

Also applies to: 97-113


67-78: getValue() swallowing exceptions may mask real storage failures.

On Line 74–77 you log and return null for any exception. Callers can’t distinguish “missing key” vs “DB error”. If null is the intended API, consider returning an Optional-like wrapper (if feasible) or adding a dedicated error signal at the service boundary.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java (1)

67-70: Comment about “transaction-safe persistence” looks inaccurate (or needs backing).

On Line 68–69: the DAO shown uses simple find... + save(...) without any visible transaction boundary. Either tighten the comment or add a transaction mechanism if the underlying stack supports it.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0ace6a and 243b482.

📒 Files selected for processing (2)
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java
🔇 Additional comments (3)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java (2)

42-46: Nice layering: service delegates reads and permitted-job retrieval cleanly.

getValue() and getPermittedJobs() are thin delegations, which keeps validation centralized in the service while persistence stays in the DAO.

Also applies to: 72-75


14-15: Exception handling is already correct; cron expression is not used for actual scheduling.

isValidCronExpression() properly catches all exceptions and returns false rather than throwing—callers won't see thrown exceptions. More significantly, the syncFreq parameter passed to scheduleJob() (line 100) is accepted but never used. Actual scheduling uses a hardcoded 15-minute interval (JOB_PERIODIC_SECONDS, line 109–113), not the cron expression. Cron validation is only applied for UI display of next sync time calculation (line 177–181) and pre-validation in LocalConfigServiceImpl.modifyJob() (line 57–59). There is no dialect mismatch with the scheduler because the validated cron expression never reaches the scheduler; Android's JobScheduler does not support cron expressions.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)

97-113: This concern is incorrect—the database schema does not enforce uniqueness constraints on name or (name, configType).

The local_preferences table (from ClientDatabaseMigrations.java) only has a PRIMARY KEY on id. Each insert() call uses a new UUID for the id field, so there is no primary key conflict. Multiple rows with the same name can safely coexist with different id values.

The code uses soft deletes (setting is_deleted = 1), which means deleted records remain in the database. While resurrecting deleted records would be a cleaner design pattern, the current implementation will not fail at runtime due to uniqueness violations.

Likely an incorrect or invalid review comment.

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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: 1

🤖 Fix all issues with AI agents
In
@android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java:
- Around line 67-78: getValue currently queries only by name
(LocalConfigDAOImpl.getValue) and can return ambiguous results; change it to
accept a configType parameter (or add an overloaded getValue(name, configType))
and call
localPreferencesRepository.findByIsDeletedFalseAndNameAndConfigType(name,
configType) so behavior matches modifyJob/modifyConfigurations and prevents JOB
vs CONFIGURATION cross-contamination; update callers to pass the appropriate
ConfigType where necessary.
🧹 Nitpick comments (1)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)

97-115: Consider using update() instead of save() for existing records.

On Line 110, when updating an existing preference, the code calls save() (which delegates to Room's insert with REPLACE strategy). While this works, it's semantically clearer and potentially more efficient to call update() for existing records.

♻️ Proposed fix
         if (existingPreference != null) {
             // Update existing record
             existingPreference.setVal(value);
             existingPreference.setUpdBy(RegistrationConstants.JOB_TRIGGER_POINT_USER);
             existingPreference.setUpdDtimes(System.currentTimeMillis());
-            localPreferencesRepository.save(existingPreference);
+            localPreferencesRepository.update(existingPreference);
         } else {
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 243b482 and 6b8bdda.

📒 Files selected for processing (4)
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalPreferencesDao.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/LocalPreferencesRepository.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java
🔇 Additional comments (5)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/LocalPreferencesRepository.java (1)

58-69: LGTM! Clean implementation with proper error handling.

The new method correctly delegates to the DAO layer and includes appropriate exception handling and logging. The JavaDoc clearly explains the purpose of preventing cross-contamination between JOB and CONFIGURATION preferences.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalPreferencesDao.java (1)

28-29: LGTM! Well-formed Room query.

The query correctly filters by all three criteria (is_deleted, name, and config_type) with proper parameter binding. This enables config-type scoped lookups as needed for the JOB vs CONFIGURATION segregation.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (3)

53-65: Good refactoring to consolidate update logic.

The extraction of the update-or-create logic into saveOrUpdateLocalPreference improves maintainability and ensures consistent behavior across configuration and job modifications.


80-95: Good input validation and error handling.

The method properly validates inputs and uses the config-type aware helper. The RuntimeException on failure ensures callers are aware of critical persistence errors.

Note: The method doesn't validate the value format (e.g., if it should be a valid cron expression). If format validation is required, consider adding it here or ensure it's performed by callers.


137-167: Excellent improvement with config-type aware cleanup.

The updated logic now correctly scopes cleanup operations to CONFIGURATION type preferences only, preventing accidental deletion of JOB preferences. The added logging provides valuable observability for cleanup operations.

Signed-off-by: sachin.sp <sachin.sp@cyberpwn.com>
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: 1

🤖 Fix all issues with AI agents
In
@android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java:
- Around line 461-489: modifyJobCronExpression currently persists cronExpression
without validation; before calling localConfigService.modifyJob(jobId,
cronExpression) validate the cronExpression (e.g., via the existing cron
utility/parser in the project or a CronExpression validator) and if invalid
return result.error with a clear message instead of modifying storage. If valid
proceed to call localConfigService.modifyJob, then continue with fetching
SyncJobDef and rescheduling; ensure you reference the cronExpression parameter
and abort early on invalid input.
🧹 Nitpick comments (2)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (1)

81-96: Consider trimming the value before storage.

The validation checks if value.trim().isEmpty() but stores the original value without trimming. This could result in storing whitespace-padded cron expressions.

🔧 Proposed fix
     @Override
     public void modifyJob(String name, String value) {
         if (name == null || name.trim().isEmpty()) {
             throw new IllegalArgumentException("Job name cannot be null or empty");
         }
         if (value == null || value.trim().isEmpty()) {
             throw new IllegalArgumentException("Job value cannot be null or empty");
         }

         try {
-            saveOrUpdateLocalPreference(name, value, RegistrationConstants.PERMITTED_JOB_TYPE);
+            saveOrUpdateLocalPreference(name.trim(), value.trim(), RegistrationConstants.PERMITTED_JOB_TYPE);
         } catch (Exception e) {
             Log.e(TAG, "Error modifying job: " + name, e);
             throw new RuntimeException("Failed to modify job: " + name, e);
         }
     }
android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java (1)

491-501: Consider: Hardcoded config type limits API flexibility.

The getValue method hardcodes PERMITTED_JOB_TYPE, which the comment explains is intentional for job cron expressions. However, this limits the API's reusability. If future use cases need to retrieve other config types, a separate method or an additional parameter would be required.

If broader use is anticipated, consider accepting configType as a parameter from the Flutter side:

@Override
public void getValue(@NonNull String name, @NonNull String configType, @NonNull MasterDataSyncPigeon.Result<String> result) {
    try {
        String value = localConfigService.getValue(name, configType);
        result.success(value);
    } catch (Exception e) {
        Log.e(TAG, "Failed to get value for: " + name, e);
        result.error(e);
    }
}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b8bdda and 6b3bdf7.

📒 Files selected for processing (7)
  • android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java
  • android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAO.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/spi/LocalConfigService.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java
  • android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java
🔇 Additional comments (11)
android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAO.java (1)

29-43: LGTM!

The new interface methods getValue and modifyJob are well-documented with clear Javadoc comments explaining parameters and return values. The method signatures align with the implementation in LocalConfigDAOImpl.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/spi/LocalConfigService.java (1)

25-45: LGTM!

The new service interface methods provide a clean abstraction layer. The getValue, modifyJob, and getPermittedJobs methods are well-documented and align with the DAO layer and dependent API implementations.

android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java (2)

38-50: LGTM!

The LocalConfigService dependency is properly injected via the constructor with the @Inject annotation. The field assignment follows the existing pattern in this class.


208-229: Proper fallback logic for custom cron expressions.

The implementation correctly:

  1. Fetches the default cron from the database first
  2. Checks for custom cron override only if localConfigService is non-null
  3. Uses syncJob.getId() (not the API name) to retrieve the custom value, which aligns with how modifyJob stores values by job ID

The null and empty string checks on customCron at line 220 prevent accidental blank overrides.

android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java (3)

67-79: LGTM!

The getValue implementation correctly uses configType-aware lookup and handles exceptions gracefully by logging and returning null. This aligns with the method contract documented in the interface.


98-116: LGTM!

The saveOrUpdateLocalPreference helper method properly consolidates the update-or-create logic with configType awareness. The Javadoc correctly documents the prevention of cross-contamination between JOB and CONFIGURATION preferences.


138-168: Clean-up logic correctly scoped to CONFIGURATION type.

The updated cleanUpLocalPreferences method now uses configType-aware lookup (line 151-152), ensuring that only PERMITTED_CONFIG_TYPE preferences are cleaned up. This prevents accidental deletion of JOB-type preferences during cleanup operations.

android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java (4)

105-152: LGTM!

The LocalConfigService dependency is properly injected and assigned, following the existing pattern for other dependencies in this class.


439-448: LGTM!

The getPermittedJobs method properly handles null by returning an empty list, preventing NPEs on the Flutter side.


450-459: LGTM!

The isValidCronExpression method delegates to CronExpressionParser and returns false on exceptions rather than propagating errors. This is appropriate for a validation endpoint.


587-597: LGTM!

The getJobIdByApiName method now uses the repository's getSyncJobDefByApiName method for a cleaner lookup. The fallback to an empty string on error is consistent with existing behavior.

@ase-101 ase-101 merged commit 9507421 into mosip:develop Jan 9, 2026
2 checks passed
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.

3 participants