diff --git a/android/app/src/main/java/io/mosip/registration_client/HostApiModule.java b/android/app/src/main/java/io/mosip/registration_client/HostApiModule.java index 30a49ebb5..47c06f31b 100644 --- a/android/app/src/main/java/io/mosip/registration_client/HostApiModule.java +++ b/android/app/src/main/java/io/mosip/registration_client/HostApiModule.java @@ -199,7 +199,7 @@ MasterDataSyncApi getSyncResponseApi( AuditManagerService auditManagerService, MasterDataService masterDataService, PacketService packetService, - GlobalParamDao globalParamDao, FileSignatureDao fileSignatureDao,PreRegistrationDataSyncService preRegistrationDataSyncService) { + GlobalParamDao globalParamDao, FileSignatureDao fileSignatureDao,PreRegistrationDataSyncService preRegistrationDataSyncService, LocalConfigService localConfigService) { return new MasterDataSyncApi(clientCryptoManagerService, machineRepository, registrationCenterRepository, syncRestService, certificateManagerService, @@ -209,7 +209,7 @@ MasterDataSyncApi getSyncResponseApi( templateRepository, dynamicFieldRepository, locationRepository, blocklistedWordRepository, syncJobDefRepository, languageRepository, jobManagerService, - auditManagerService, masterDataService, packetService, globalParamDao, fileSignatureDao, preRegistrationDataSyncService + auditManagerService, masterDataService, packetService, globalParamDao, fileSignatureDao, preRegistrationDataSyncService, localConfigService ); } diff --git a/android/app/src/main/java/io/mosip/registration_client/MainActivity.java b/android/app/src/main/java/io/mosip/registration_client/MainActivity.java index 25598da76..b43343ae3 100644 --- a/android/app/src/main/java/io/mosip/registration_client/MainActivity.java +++ b/android/app/src/main/java/io/mosip/registration_client/MainActivity.java @@ -214,12 +214,27 @@ public void onReceive(Context context, Intent intent) { } }; + private BroadcastReceiver rescheduleReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String jobApiName = intent.getStringExtra(UploadBackgroundService.EXTRA_JOB_API_NAME); + if (jobApiName != null) { + Log.d(getClass().getSimpleName(), "Rescheduling job due to cron change: " + jobApiName); + createBackgroundTask(jobApiName); + } + } + }; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // createBackgroundTask("registrationPacketUploadJob"); IntentFilter intentFilterUpload = new IntentFilter("SYNC_JOB_TRIGGER"); registerReceiver(broadcastReceiver, intentFilterUpload); + + // Register receiver for rescheduling jobs when cron expression changes + IntentFilter rescheduleFilter = new IntentFilter("RESCHEDULE_JOB"); + registerReceiver(rescheduleReceiver, rescheduleFilter); } private void initializeAutoSync() { @@ -264,6 +279,11 @@ void scheduleAllActiveJobs() { protected void onDestroy() { super.onDestroy(); unregisterReceiver(broadcastReceiver); + try { + unregisterReceiver(rescheduleReceiver); + } catch (Exception e) { + // Receiver might not be registered, ignore + } } public void createBackgroundTask(String api){ diff --git a/android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java b/android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java index 4504dcfdd..551c86628 100644 --- a/android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java +++ b/android/app/src/main/java/io/mosip/registration_client/api_services/MasterDataSyncApi.java @@ -59,10 +59,12 @@ import io.mosip.registration.clientmanager.service.JobManagerServiceImpl; import io.mosip.registration.clientmanager.spi.AuditManagerService; import io.mosip.registration.clientmanager.spi.JobManagerService; +import io.mosip.registration.clientmanager.spi.LocalConfigService; import io.mosip.registration.clientmanager.spi.MasterDataService; import io.mosip.registration.clientmanager.spi.PacketService; import io.mosip.registration.clientmanager.spi.PreRegistrationDataSyncService; import io.mosip.registration.clientmanager.spi.SyncRestService; +import io.mosip.registration.clientmanager.util.CronExpressionParser; import io.mosip.registration.keymanager.spi.CertificateManagerService; import io.mosip.registration.keymanager.spi.ClientCryptoManagerService; import io.mosip.registration_client.utils.BatchJob; @@ -100,6 +102,7 @@ public class MasterDataSyncApi implements MasterDataSyncPigeon.SyncApi { GlobalParamDao globalParamDao; FileSignatureDao fileSignatureDao; PreRegistrationDataSyncService preRegistrationDataSyncService; + LocalConfigService localConfigService; Context context; private String regCenterId; @@ -120,7 +123,7 @@ public MasterDataSyncApi(ClientCryptoManagerService clientCryptoManagerService, AuditManagerService auditManagerService, MasterDataService masterDataService, PacketService packetService, - GlobalParamDao globalParamDao, FileSignatureDao fileSignatureDao, PreRegistrationDataSyncService preRegistrationDataSyncService) { + GlobalParamDao globalParamDao, FileSignatureDao fileSignatureDao, PreRegistrationDataSyncService preRegistrationDataSyncService, LocalConfigService localConfigService) { this.clientCryptoManagerService = clientCryptoManagerService; this.machineRepository = machineRepository; this.registrationCenterRepository = registrationCenterRepository; @@ -146,6 +149,7 @@ public MasterDataSyncApi(ClientCryptoManagerService clientCryptoManagerService, this.globalParamDao = globalParamDao; this.fileSignatureDao = fileSignatureDao; this.preRegistrationDataSyncService = preRegistrationDataSyncService; + this.localConfigService = localConfigService; } public void setCallbackActivity(MainActivity mainActivity, BatchJob batchJob) { @@ -432,6 +436,70 @@ public void getActiveSyncJobs(@NonNull MasterDataSyncPigeon.Result> result.success(value); } + @Override + public void getPermittedJobs(@NonNull MasterDataSyncPigeon.Result> result) { + try { + List permittedJobs = localConfigService.getPermittedJobs(); + result.success(permittedJobs != null ? permittedJobs : new ArrayList<>()); + } catch (Exception e) { + Log.e(TAG, "Failed to get permitted jobs", e); + result.error(e); + } + } + + @Override + public void isValidCronExpression(@NonNull String cronExpression, @NonNull MasterDataSyncPigeon.Result result) { + try { + boolean isValid = CronExpressionParser.isValidCronExpression(cronExpression); + result.success(isValid); + } catch (Exception e) { + Log.e(TAG, "Error validating cron expression", e); + result.success(false); + } + } + + @Override + public void modifyJobCronExpression(@NonNull String jobId, @NonNull String cronExpression, @NonNull MasterDataSyncPigeon.Result result) { + try { + localConfigService.modifyJob(jobId, cronExpression); + + // Fetch specific sync job definition by jobId + SyncJobDef jobDef = syncJobDefRepository.getSyncJobDefById(jobId); + + if (jobDef != null) { + // Refresh job status to apply new cron expression + // This will reschedule JobScheduler jobs with new cron + jobManagerService.refreshJobStatus(jobDef); + + // For AlarmManager-based jobs (like batch jobs), reschedule immediately + if (jobDef.getApiName() != null && activity != null) { + // Reschedule using MainActivity's createBackgroundTask via broadcast + Intent rescheduleIntent = new Intent("RESCHEDULE_JOB"); + rescheduleIntent.putExtra(UploadBackgroundService.EXTRA_JOB_API_NAME, jobDef.getApiName()); + context.sendBroadcast(rescheduleIntent); + Log.d(TAG, "Sent reschedule broadcast for job: " + jobDef.getApiName()); + } + } + + result.success(true); + } catch (Exception e) { + Log.e(TAG, "Failed to modify job cron expression", e); + result.error(e); + } + } + + @Override + public void getValue(@NonNull String name, @NonNull MasterDataSyncPigeon.Result result) { + try { + // getValue is used for retrieving job cron expressions, so use PERMITTED_JOB_TYPE + String value = localConfigService.getValue(name, RegistrationConstants.PERMITTED_JOB_TYPE); + result.success(value); + } catch (Exception e) { + Log.e(TAG, "Failed to get value for: " + name, e); + result.error(e); + } + } + // Execute job based on API name public void executeJobByApiName(String jobApiName, Context context) { new Thread(() -> { @@ -518,11 +586,9 @@ public void executeJobByApiName(String jobApiName, Context context) { private String getJobIdByApiName(String apiName) { try { - List jobs = syncJobDefRepository.getAllSyncJobDefList(); - for (SyncJobDef job : jobs) { - if (apiName.equals(job.getApiName())) { - return job.getId(); - } + SyncJobDef job = syncJobDefRepository.getSyncJobDefByApiName(apiName); + if (job != null) { + return job.getId(); } } catch (Exception e) { Log.e(getClass().getSimpleName(), "Error getting job ID for: " + apiName, e); diff --git a/android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java b/android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java index 31d00adab..3c60a4c58 100644 --- a/android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java +++ b/android/app/src/main/java/io/mosip/registration_client/utils/BatchJob.java @@ -16,6 +16,7 @@ import io.mosip.registration.clientmanager.constant.Components; import io.mosip.registration.clientmanager.constant.PacketClientStatus; import io.mosip.registration.clientmanager.constant.PacketTaskStatus; +import io.mosip.registration.clientmanager.constant.RegistrationConstants; import io.mosip.registration.clientmanager.dao.GlobalParamDao; import io.mosip.registration.clientmanager.entity.GlobalParam; import io.mosip.registration.clientmanager.entity.Registration; @@ -23,6 +24,7 @@ import io.mosip.registration.clientmanager.repository.SyncJobDefRepository; import io.mosip.registration.clientmanager.spi.AsyncPacketTaskCallBack; import io.mosip.registration.clientmanager.spi.AuditManagerService; +import io.mosip.registration.clientmanager.spi.LocalConfigService; import io.mosip.registration.clientmanager.spi.PacketService; import io.mosip.registration_client.MainActivity; import io.mosip.registration_client.R; @@ -33,16 +35,19 @@ public class BatchJob { AuditManagerService auditManagerService; GlobalParamDao globalParamDao; SyncJobDefRepository syncJobDefRepository; + LocalConfigService localConfigService; Activity activity; boolean syncAndUploadInProgressStatus = false; @Inject public BatchJob(PacketService packetService, AuditManagerService auditManagerService, - GlobalParamDao globalParamDao, SyncJobDefRepository syncJobDefRepository) { + GlobalParamDao globalParamDao, SyncJobDefRepository syncJobDefRepository, + LocalConfigService localConfigService) { this.packetService = packetService; this.auditManagerService = auditManagerService; this.globalParamDao = globalParamDao; this.syncJobDefRepository = syncJobDefRepository; + this.localConfigService = localConfigService; } public void setCallbackActivity(MainActivity mainActivity) { @@ -203,12 +208,19 @@ public Integer getBatchSize() { public long getIntervalMillis(String api) { // Default everyday at Noon - 12pm String cronExp = ClientManagerConstant.DEFAULT_UPLOAD_CRON; - List syncJobs = syncJobDefRepository.getAllSyncJobDefList(); - for (SyncJobDef value : syncJobs) { - if (Objects.equals(value.getApiName(), api)) { - Log.d(getClass().getSimpleName(), api + " Cron Expression : " + String.valueOf(value.getSyncFreq())); - cronExp = String.valueOf(value.getSyncFreq()); - break; + SyncJobDef syncJob = syncJobDefRepository.getSyncJobDefByApiName(api); + if (syncJob != null) { + // Use default from DB first + cronExp = String.valueOf(syncJob.getSyncFreq()); + Log.d(getClass().getSimpleName(), api + " Default Cron Expression : " + cronExp); + + // Check for custom cron expression and override if available + if (localConfigService != null) { + String customCron = localConfigService.getValue(syncJob.getId(), RegistrationConstants.PERMITTED_JOB_TYPE); + if (customCron != null && !customCron.trim().isEmpty()) { + cronExp = customCron; // Use custom cron expression + Log.d(getClass().getSimpleName(), api + " Custom Cron Expression : " + cronExp); + } } } long nextExecution = CronParserUtil.getNextExecutionTimeInMillis(cronExp); diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/config/AppModule.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/config/AppModule.java index 6a05a0ff6..aa17ab068 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/config/AppModule.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/config/AppModule.java @@ -53,6 +53,7 @@ import io.mosip.registration.clientmanager.service.external.PreRegZipHandlingService; import io.mosip.registration.clientmanager.service.external.impl.PreRegZipHandlingServiceImpl; import io.mosip.registration.clientmanager.spi.AuditManagerService; +import io.mosip.registration.clientmanager.spi.LocalConfigService; import io.mosip.registration.clientmanager.spi.JobManagerService; import io.mosip.registration.clientmanager.spi.JobTransactionService; import io.mosip.registration.clientmanager.spi.LocationValidationService; @@ -259,8 +260,8 @@ DateUtil provideDateUtil() { @Provides @Singleton - JobManagerService provideJobManagerService(SyncJobDefRepository syncJobDefRepository, JobTransactionService jobTransactionService, DateUtil dateUtil) { - return new JobManagerServiceImpl(appContext, syncJobDefRepository, jobTransactionService, dateUtil); + JobManagerService provideJobManagerService(SyncJobDefRepository syncJobDefRepository, JobTransactionService jobTransactionService, DateUtil dateUtil, LocalConfigService localConfigService) { + return new JobManagerServiceImpl(appContext, syncJobDefRepository, jobTransactionService, dateUtil, localConfigService); } @Provides diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAO.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAO.java index 357960bb3..5f933d945 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAO.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAO.java @@ -26,5 +26,20 @@ public interface LocalConfigDAO { */ void modifyConfigurations(Map localPreferences); + /** + * Get value for a specific local preference by name and config type + * @param name Preference name + * @param configType Configuration type (PERMITTED_JOB_TYPE or PERMITTED_CONFIG_TYPE) + * @return Preference value or null if not found + */ + String getValue(String name, String configType); + + /** + * Modify job cron expression + * @param name Job ID + * @param value Cron expression value + */ + void modifyJob(String name, String value); + void cleanUpLocalPreferences(); } diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java index 04b328f41..80de8ac93 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalConfigDAOImpl.java @@ -25,15 +25,15 @@ public class LocalConfigDAOImpl implements LocalConfigDAO { @Inject public LocalConfigDAOImpl(PermittedLocalConfigRepository permittedLocalConfigRepository, - LocalPreferencesRepository localPreferencesRepository) { + LocalPreferencesRepository localPreferencesRepository) { this.permittedLocalConfigRepository = permittedLocalConfigRepository; this.localPreferencesRepository = localPreferencesRepository; } @Override public List getPermittedConfigurations(String configType) { - List permittedConfigs = - permittedLocalConfigRepository.getPermittedConfigsByType(configType); + List permittedConfigs = permittedLocalConfigRepository + .getPermittedConfigsByType(configType); List permittedConfigurations = new ArrayList<>(); if (permittedConfigs != null && !permittedConfigs.isEmpty()) { @@ -51,31 +51,69 @@ public Map getLocalConfigurations() { @Override public void modifyConfigurations(Map localPreferences) { - + for (Map.Entry entry : localPreferences.entrySet()) { String name = entry.getKey(); String value = entry.getValue(); - + try { - LocalPreferences existingPreference = localPreferencesRepository.findByIsDeletedFalseAndName(name); - - if (existingPreference != null) { - // Update existing record - existingPreference.setVal(value); - existingPreference.setUpdBy(RegistrationConstants.JOB_TRIGGER_POINT_USER); - existingPreference.setUpdDtimes(System.currentTimeMillis()); - localPreferencesRepository.save(existingPreference); - } else { - // Create new record if it doesn't exist - saveLocalPreference(name, value, RegistrationConstants.PERMITTED_CONFIG_TYPE); - } - + saveOrUpdateLocalPreference(name, value, RegistrationConstants.PERMITTED_CONFIG_TYPE); } catch (Exception e) { Log.e(TAG, "Error modifying configuration: " + name, e); } } } + @Override + public String getValue(String name, String configType) { + try { + LocalPreferences localPreference = localPreferencesRepository + .findByIsDeletedFalseAndNameAndConfigType(name, configType); + if (localPreference != null && localPreference.getVal() != null) { + return localPreference.getVal(); + } + } catch (Exception e) { + Log.e(TAG, "Error getting value for: " + name + ", configType: " + configType, e); + } + return null; + } + + @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); + } catch (Exception e) { + Log.e(TAG, "Error modifying job: " + name, e); + throw new RuntimeException("Failed to modify job: " + name, e); + } + } + + /** + * Save local preference to database + * Uses configType-aware lookup to prevent cross-contamination between JOB and CONFIGURATION preferences + */ + private void saveOrUpdateLocalPreference(String name, String value, String configType) { + LocalPreferences existingPreference = localPreferencesRepository + .findByIsDeletedFalseAndNameAndConfigType(name, configType); + + if (existingPreference != null) { + // Update existing record + existingPreference.setVal(value); + existingPreference.setUpdBy(RegistrationConstants.JOB_TRIGGER_POINT_USER); + existingPreference.setUpdDtimes(System.currentTimeMillis()); + localPreferencesRepository.save(existingPreference); + } else { + // Create new record if it doesn't exist + saveLocalPreference(name, value, configType); + } + } /** * Save local preference to database @@ -88,7 +126,7 @@ private void saveLocalPreference(String name, String value, String configType) { localPreference.setCrBy(RegistrationConstants.JOB_TRIGGER_POINT_USER); localPreference.setCrDtime(System.currentTimeMillis()); localPreference.setIsDeleted(false); - + localPreferencesRepository.save(localPreference); } @@ -98,8 +136,8 @@ private void saveLocalPreference(String name, String value, String configType) { * Mark as deleted if key is deactivated in permitted configs. */ public void cleanUpLocalPreferences() { - List permittedConfigs = - permittedLocalConfigRepository.getPermittedConfigsByType(RegistrationConstants.PERMITTED_CONFIG_TYPE); + List permittedConfigs = permittedLocalConfigRepository + .getPermittedConfigsByType(RegistrationConstants.PERMITTED_CONFIG_TYPE); Map localConfigs = getLocalConfigurations(); @@ -109,8 +147,11 @@ public void cleanUpLocalPreferences() { } for (String key : localConfigs.keySet()) { - LocalPreferences pref = localPreferencesRepository.findByIsDeletedFalseAndName(key); - if (pref == null) continue; + // Use configType-aware lookup to ensure we only clean up CONFIGURATION type preferences + LocalPreferences pref = localPreferencesRepository + .findByIsDeletedFalseAndNameAndConfigType(key, RegistrationConstants.PERMITTED_CONFIG_TYPE); + if (pref == null) + continue; if (!permittedStatusMap.containsKey(key)) { localPreferencesRepository.delete(pref); diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalPreferencesDao.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalPreferencesDao.java index f283f269d..ed2b5d350 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalPreferencesDao.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/LocalPreferencesDao.java @@ -25,6 +25,9 @@ public interface LocalPreferencesDao { @Query("SELECT * FROM local_preferences WHERE is_deleted = 0 AND name = :name") LocalPreferences findByIsDeletedFalseAndName(String name); + @Query("SELECT * FROM local_preferences WHERE is_deleted = 0 AND name = :name AND config_type = :configType") + LocalPreferences findByIsDeletedFalseAndNameAndConfigType(String name, String configType); + @Query("DELETE FROM local_preferences WHERE name = :name") void deleteByName(String name); } diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/SyncJobDefDao.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/SyncJobDefDao.java index 8e6b35310..08ede1eea 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/SyncJobDefDao.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/dao/SyncJobDefDao.java @@ -28,11 +28,21 @@ public interface SyncJobDefDao { /** * To get a job in the List of {@link SyncJobDef} * + * @param jobId the job id * @return sync job */ - @Query("select * from sync_job_def where is_deleted == 0 or is_deleted is null and id=:jobId") + @Query("select * from sync_job_def where (is_deleted == 0 or is_deleted is null) and id=:jobId") SyncJobDef findOneById(String jobId); + /** + * To get a job by API name in the List of {@link SyncJobDef} + * + * @param apiName the API name + * @return sync job + */ + @Query("select * from sync_job_def where (is_deleted == 0 or is_deleted is null) and api_name=:apiName") + SyncJobDef findOneByApiName(String apiName); + /** * To get all the List of active {@link SyncJobDef} * diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/LocalPreferencesRepository.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/LocalPreferencesRepository.java index 55f9431b4..4df98c260 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/LocalPreferencesRepository.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/LocalPreferencesRepository.java @@ -55,6 +55,19 @@ public LocalPreferences findByIsDeletedFalseAndName(String name) { } } + /** + * Find local preference by name and config type + * This prevents cross-contamination between JOB and CONFIGURATION preferences + */ + public LocalPreferences findByIsDeletedFalseAndNameAndConfigType(String name, String configType) { + try { + return localPreferencesDao.findByIsDeletedFalseAndNameAndConfigType(name, configType); + } catch (Exception e) { + Log.e(TAG, "Error finding local preference by name and config type: " + name + ", " + configType, e); + return null; + } + } + /** * Save local preference */ diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/SyncJobDefRepository.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/SyncJobDefRepository.java index ce561fa2b..7e65b2b38 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/SyncJobDefRepository.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/SyncJobDefRepository.java @@ -36,4 +36,24 @@ public List getActiveSyncJobs() { List activeJobs = this.syncJobDefDao.findAllByActiveStatus(true); return activeJobs; } + + /** + * Get a sync job definition by job ID + * + * @param jobId the job ID + * @return sync job definition or null if not found + */ + public SyncJobDef getSyncJobDefById(String jobId) { + return this.syncJobDefDao.findOneById(jobId); + } + + /** + * Get a sync job definition by API name + * + * @param apiName the API name + * @return sync job definition or null if not found + */ + public SyncJobDef getSyncJobDefByApiName(String apiName) { + return this.syncJobDefDao.findOneByApiName(apiName); + } } diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java index bad4880b2..8e0d6f059 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/JobManagerServiceImpl.java @@ -17,6 +17,7 @@ import java.util.concurrent.TimeUnit; import io.mosip.registration.clientmanager.R; +import io.mosip.registration.clientmanager.constant.RegistrationConstants; import io.mosip.registration.clientmanager.entity.SyncJobDef; import io.mosip.registration.clientmanager.jobs.ConfigDataSyncJob; import io.mosip.registration.clientmanager.jobs.DeleteAuditLogsJob; @@ -25,6 +26,7 @@ import io.mosip.registration.clientmanager.repository.SyncJobDefRepository; import io.mosip.registration.clientmanager.spi.JobManagerService; import io.mosip.registration.clientmanager.spi.JobTransactionService; +import io.mosip.registration.clientmanager.spi.LocalConfigService; import io.mosip.registration.clientmanager.util.CronExpressionParser; import io.mosip.registration.clientmanager.util.DateUtil; @@ -44,13 +46,15 @@ public class JobManagerServiceImpl implements JobManagerService { JobTransactionService jobTransactionService; SyncJobDefRepository syncJobDefRepository; DateUtil dateUtil; + LocalConfigService localConfigService; - public JobManagerServiceImpl(Context context, SyncJobDefRepository syncJobDefRepository, JobTransactionService jobTransactionService, DateUtil dateUtil) { + public JobManagerServiceImpl(Context context, SyncJobDefRepository syncJobDefRepository, JobTransactionService jobTransactionService, DateUtil dateUtil, LocalConfigService localConfigService) { this.context = context; this.jobScheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE); this.syncJobDefRepository = syncJobDefRepository; this.jobTransactionService = jobTransactionService; this.dateUtil = dateUtil; + this.localConfigService = localConfigService; } /** @@ -81,8 +85,10 @@ public void refreshJobStatus(SyncJobDef jobDef) { return; } + // Use getSyncFrequency to check for custom cron expression first + String syncFreq = getSyncFrequency(jobDef); if (!isJobScheduled(jobId)) - scheduleJob(jobId, jobDef.getApiName(), jobDef.getSyncFreq()); + scheduleJob(jobId, jobDef.getApiName(), syncFreq); } /** @@ -167,7 +173,7 @@ public String getNextSyncTime(int jobId) { return "NA"; } - String cronExpression = jobDef.getSyncFreq(); + String cronExpression = getSyncFrequency(jobDef); // Try cron-based calculation first if (CronExpressionParser.isValidCronExpression(cronExpression)) { Instant nextExecution = CronExpressionParser.getNextExecutionTime(cronExpression); @@ -185,6 +191,21 @@ public String getNextSyncTime(int jobId) { return "NA"; } + /** + * Get sync frequency for a job, checking custom cron expression first, then default + * @param syncJob Job definition + * @return Cron expression (custom if exists, otherwise default) + */ + private String getSyncFrequency(SyncJobDef syncJob) { + if (localConfigService != null) { + String localPreference = localConfigService.getValue(syncJob.getId(), RegistrationConstants.PERMITTED_JOB_TYPE); + if (localPreference != null && !localPreference.trim().isEmpty()) { + return localPreference; + } + } + return syncJob.getSyncFreq(); + } + @Override public int generateJobServiceId(String syncJobDefId) { try { diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java index 34df86013..ba2a8b710 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/LocalConfigServiceImpl.java @@ -1,5 +1,7 @@ package io.mosip.registration.clientmanager.service; +import android.util.Log; + import java.util.List; import java.util.Map; @@ -9,10 +11,12 @@ import io.mosip.registration.clientmanager.constant.RegistrationConstants; import io.mosip.registration.clientmanager.dao.LocalConfigDAO; import io.mosip.registration.clientmanager.spi.LocalConfigService; +import io.mosip.registration.clientmanager.util.CronExpressionParser; @Singleton public class LocalConfigServiceImpl implements LocalConfigService { + private static final String TAG = LocalConfigServiceImpl.class.getSimpleName(); private LocalConfigDAO localConfigDAO; @Inject @@ -34,4 +38,45 @@ public void modifyConfigurations(Map localPreferences) { public List getPermittedConfiguration() { return localConfigDAO.getPermittedConfigurations(RegistrationConstants.PERMITTED_CONFIG_TYPE); } + + @Override + public String getValue(String name, String configType) { + return localConfigDAO.getValue(name, configType); + } + + @Override + public void modifyJob(String name, String value) { + // Validate job name is not null or empty + if (name == null || name.trim().isEmpty()) { + Log.e(TAG, "Cannot modify job: job name is null or empty"); + throw new IllegalArgumentException("Job name cannot be null or empty"); + } + + // Validate cron expression before persisting to database + // This prevents invalid cron expressions from being stored, which could cause + // job scheduling failures or runtime errors when the cron is used + if (value == null || value.trim().isEmpty()) { + Log.e(TAG, "Cannot modify job " + name + ": cron expression is null or empty"); + throw new IllegalArgumentException("Cron expression cannot be null or empty"); + } + + if (!CronExpressionParser.isValidCronExpression(value)) { + Log.e(TAG, "Cannot modify job " + name + ": invalid cron expression: " + value); + throw new IllegalArgumentException("Invalid cron expression: " + value); + } + + if (!getPermittedJobs().contains(name)) { + Log.e(TAG, "Cannot modify job " + name + ": not a permitted job"); + throw new IllegalArgumentException("Job modification not permitted for: " + name); + } + + // Delegate to DAO only after validation passes + // The DAO layer handles the transaction-safe persistence + localConfigDAO.modifyJob(name, value); + } + + @Override + public List getPermittedJobs() { + return localConfigDAO.getPermittedConfigurations(RegistrationConstants.PERMITTED_JOB_TYPE); + } } diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/spi/LocalConfigService.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/spi/LocalConfigService.java index db3266aa3..6c5ae4196 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/spi/LocalConfigService.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/spi/LocalConfigService.java @@ -22,4 +22,25 @@ public interface LocalConfigService { * Get permitted configuration names */ List getPermittedConfiguration(); + + /** + * Get value for a specific local preference by name and config type + * @param name Preference name + * @param configType Configuration type (PERMITTED_JOB_TYPE or PERMITTED_CONFIG_TYPE) + * @return Preference value or null if not found + */ + String getValue(String name, String configType); + + /** + * Modify job cron expression + * @param name Job ID + * @param value Cron expression value + */ + void modifyJob(String name, String value); + + /** + * Get permitted job IDs + * @return List of permitted job IDs that can be edited + */ + List getPermittedJobs(); } diff --git a/lib/platform_android/sync_response_service_impl.dart b/lib/platform_android/sync_response_service_impl.dart index ea83a38c9..d33ab8227 100644 --- a/lib/platform_android/sync_response_service_impl.dart +++ b/lib/platform_android/sync_response_service_impl.dart @@ -271,6 +271,62 @@ class SyncResponseServiceImpl implements SyncResponseService { return "false"; } } + + @override + Future> getPermittedJobs() async { + try { + final permittedJobs = await SyncApi().getPermittedJobs(); + return permittedJobs; + } on PlatformException catch (e) { + debugPrint('getPermittedJobs PlatformException: ${e.message}'); + return []; + } catch (e) { + debugPrint('getPermittedJobs failed: $e'); + return []; + } + } + + @override + Future isValidCronExpression(String cronExpression) async { + try { + final isValid = await SyncApi().isValidCronExpression(cronExpression); + return isValid; + } on PlatformException catch (e) { + debugPrint('isValidCronExpression PlatformException: ${e.message}'); + return false; + } catch (e) { + debugPrint('isValidCronExpression failed: $e'); + return false; + } + } + + @override + Future modifyJobCronExpression(String jobId, String cronExpression) async { + try { + final success = await SyncApi().modifyJobCronExpression(jobId, cronExpression); + return success; + } on PlatformException catch (e) { + debugPrint('modifyJobCronExpression PlatformException: ${e.message}'); + return false; + } catch (e) { + debugPrint('modifyJobCronExpression failed: $e'); + return false; + } + } + + @override + Future getValue(String name) async { + try { + final value = await SyncApi().getValue(name); + return value; + } on PlatformException catch (e) { + debugPrint('getValue PlatformException: ${e.message}'); + return null; + } catch (e) { + debugPrint('getValue failed: $e'); + return null; + } + } } SyncResponseService getSyncResponseServiceImpl() => SyncResponseServiceImpl(); diff --git a/lib/platform_spi/sync_response_service.dart b/lib/platform_spi/sync_response_service.dart index 01efd7bc4..872cab2f6 100644 --- a/lib/platform_spi/sync_response_service.dart +++ b/lib/platform_spi/sync_response_service.dart @@ -32,5 +32,10 @@ abstract class SyncResponseService { Future getLastSyncTimeByJobId(String jobId); Future getNextSyncTimeByJobId(String jobId); + Future> getPermittedJobs(); + Future isValidCronExpression(String cronExpression); + Future modifyJobCronExpression(String jobId, String cronExpression); + Future getValue(String name); + factory SyncResponseService() => getSyncResponseServiceImpl(); } \ No newline at end of file diff --git a/lib/provider/sync_provider.dart b/lib/provider/sync_provider.dart index 2f85874a5..f7b14ea80 100644 --- a/lib/provider/sync_provider.dart +++ b/lib/provider/sync_provider.dart @@ -5,6 +5,7 @@ * */ +import 'dart:async'; import 'dart:convert'; import 'dart:developer'; @@ -34,6 +35,9 @@ class SyncProvider with ChangeNotifier { bool isSyncInProgress = false; bool _isSyncAndUploadInProgress = false; + Timer? _jobStatusPollingTimer; + final Map _jobStatuses = {}; + String get lastSuccessfulSyncTime => _lastSuccessfulSyncTime; int get currentSyncProgress => _currentSyncProgress; String get currentProgressType => _currentProgressType; @@ -48,6 +52,8 @@ class SyncProvider with ChangeNotifier { bool get cacertsSyncSuccess => _cacertsSyncSuccess; bool get kernelCertsSyncSuccess => _kernelCertsSyncSuccess; + Map get jobStatuses => _jobStatuses; + set isSyncing(bool value) { _isSyncing = value; notifyListeners(); @@ -280,4 +286,56 @@ class SyncProvider with ChangeNotifier { return null; } } + + void startJobPolling() { + refreshJobStatuses(); // Initial fetch + _jobStatusPollingTimer?.cancel(); + _jobStatusPollingTimer = Timer.periodic(const Duration(seconds: 30), (_) { + refreshJobStatuses(); + }); + } + + void stopJobPolling() { + _jobStatusPollingTimer?.cancel(); + _jobStatusPollingTimer = null; + } + + Future refreshJobStatuses() async { + try { + final activeJobs = await syncResponseService.getActiveSyncJobs(); + for (final jobJson in activeJobs) { + if (jobJson == null) continue; + try { + final job = SyncJobDef.fromJson(json.decode(jobJson) as Map); + 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"); + } + } + + await getLastSyncTime(); // Update global last sync time (fallback for Master Sync) + notifyListeners(); + } catch (e) { + log("Error refreshing job statuses: $e"); + } + } + + @override + void dispose() { + stopJobPolling(); + super.dispose(); + } +} + +class JobStatus { + final String id; + final String? lastSyncTime; + final String? nextSyncTime; + + JobStatus({required this.id, this.lastSyncTime, this.nextSyncTime}); } diff --git a/lib/ui/settings/widgets/scheduled_jobs_settings.dart b/lib/ui/settings/widgets/scheduled_jobs_settings.dart index b8041bb39..e1e135ebf 100644 --- a/lib/ui/settings/widgets/scheduled_jobs_settings.dart +++ b/lib/ui/settings/widgets/scheduled_jobs_settings.dart @@ -5,13 +5,14 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import 'package:registration_client/platform_spi/sync_response_service.dart'; import 'package:registration_client/utils/sync_job_def.dart'; +import 'package:restart_app/restart_app.dart'; import '../../../provider/sync_provider.dart'; // Dart equivalent of the Java PACKET_JOBS constant const List PACKET_JOBS = ['RPS_J00006', 'RSJ_J00014', 'PUJ_J00017']; -class ScheduledJobsSettings extends StatelessWidget { +class ScheduledJobsSettings extends StatefulWidget { const ScheduledJobsSettings({ super.key, required this.jobJsonList, @@ -21,19 +22,55 @@ class ScheduledJobsSettings extends StatelessWidget { final List jobJsonList; final void Function(String jobId)? onRefreshJob; + @override + State createState() => _ScheduledJobsSettingsState(); +} + +class _ScheduledJobsSettingsState extends State { + List _permittedJobs = []; + bool _isLoadingPermittedJobs = true; + SyncProvider? _syncProvider; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _syncProvider = context.read(); + _syncProvider?.startJobPolling(); + }); + _loadPermittedJobs(); + } + + Future _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'); + setState(() { + _isLoadingPermittedJobs = false; + }); + } + } + + @override + void dispose() { + _syncProvider?.stopJobPolling(); + super.dispose(); + } + @override Widget build(BuildContext context) { - final jobs = jobJsonList + final jobs = widget.jobJsonList .whereType() .map((e) => _ScheduledJob.fromJson(json.decode(e) as Map)) .toList(); - final bottomInset = MediaQuery.of(context).padding.bottom; - final bottomSpacer = bottomInset + kBottomNavigationBarHeight + 230; - final mediaSize = MediaQuery.of(context).size; - final bool isTablet = mediaSize.shortestSide <= 450; - final int crossAxisCount = isTablet ? 1 : 2; - final double childAspectRatio = MediaQuery.of(context).orientation == Orientation.landscape ? 5 : 3; return SafeArea( top: false, bottom: true, @@ -58,21 +95,25 @@ class ScheduledJobsSettings extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12.0), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, + crossAxisCount: MediaQuery.of(context).size.shortestSide <= 450 ? 1 : 2, mainAxisSpacing: 8, crossAxisSpacing: 12, - childAspectRatio: childAspectRatio, + childAspectRatio: MediaQuery.of(context).orientation == Orientation.landscape ? 5 : 3, ), delegate: SliverChildBuilderDelegate( (context, index) { final job = jobs[index]; - return _JobCard(job: job, onRefresh: onRefreshJob); + return _JobCard( + job: job, + onRefresh: widget.onRefreshJob, + isPermitted: _permittedJobs.contains(job.id), + ); }, childCount: jobs.length, ), ), ), - SliverToBoxAdapter(child: SizedBox(height: bottomSpacer)), + SliverToBoxAdapter(child: SizedBox(height: MediaQuery.of(context).padding.bottom + kBottomNavigationBarHeight + 230)), ], ), ); @@ -80,9 +121,10 @@ class ScheduledJobsSettings extends StatelessWidget { } class _JobCard extends StatefulWidget { - const _JobCard({required this.job, this.onRefresh}); + const _JobCard({required this.job, this.onRefresh, required this.isPermitted}); final _ScheduledJob job; final void Function(String jobId)? onRefresh; + final bool isPermitted; @override State<_JobCard> createState() => _JobCardState(); @@ -92,45 +134,157 @@ 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; @override void initState() { super.initState(); syncProvider = Provider.of(context, listen: false); - _loadLastSyncTime(); // Fetch last sync when widget loads - _loadNextSyncTime(); + _loadCronExpression(); // Load custom cron expression or default } - Future _loadLastSyncTime() async { - if (widget.job.id != null && widget.job.id!.isNotEmpty) { - final value = await syncProvider.getLastSyncTimeByJobId(widget.job.id!); - setState(() => _lastSync = value ?? '-'); - if (widget.job.apiName == "masterSyncJob" && _lastSync == "NA") { - _lastSync = formatDate(syncProvider.lastSuccessfulSyncTime); - setState(() {}); + Future _loadCronExpression() async { + try { + if (widget.job.id != null && widget.job.id!.isNotEmpty) { + // Check for custom cron expression + 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 + } + } else { + _cronController.text = widget.job.syncFreq ?? ''; } - } else { - setState(() => _lastSync = '-'); + } catch (e) { + debugPrint('Failed to load cron expression: $e'); + // Fallback to default cron expression from job definition + _cronController.text = widget.job.syncFreq ?? ''; } } + @override + void dispose() { + _cronController.dispose(); + super.dispose(); + } + String formatDate(String dateString) { - // Parse the input UTC date string - DateTime dateTime = DateTime.parse(dateString).toLocal(); // Convert to local time + try { + // Parse the input UTC date string + DateTime dateTime = DateTime.parse(dateString).toLocal(); // Convert to local time - // Format the date - String formattedDate = DateFormat("yyyy-MMM-dd HH:mm:ss").format(dateTime); + // Format the date + String formattedDate = DateFormat("yyyy-MMM-dd HH:mm:ss").format(dateTime); - return formattedDate; + return formattedDate; + } catch(e) { + return dateString; + } } - Future _loadNextSyncTime() async { - if (widget.job.id != null && widget.job.id!.isNotEmpty) { - final value = await syncProvider.getNextSyncTimeByJobId(widget.job.id!); - setState(() => _nextSync = value ?? '-'); - } else { - setState(() => _nextSync = '-'); + Future _modifyCronExpression() async { + if (_isSaving) return; + + setState(() { + _isSaving = true; + }); + + final cronExpression = _cronController.text.trim(); + + if (cronExpression.isEmpty) { + setState(() { + _cronError = 'Cron expression cannot be empty'; + _isSaving = false; + }); + return; + } + + // Validate cron expression + final isValid = await _syncResponseService.isValidCronExpression(cronExpression); + if (!isValid) { + setState(() { + _cronError = 'Invalid cron expression'; + _isSaving = false; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Invalid cron expression')), + ); + } + return; + } + + setState(() { + _cronError = null; + }); + + // Validate job ID before proceeding + final jobId = widget.job.id; + if (jobId == null || jobId.isEmpty) { + setState(() { + _cronError = 'Job ID is required'; + _isSaving = false; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Cannot save cron expression: Job ID is missing')), + ); + } + return; + } + + // Save cron expression + try { + final success = await _syncResponseService.modifyJobCronExpression( + jobId, + cronExpression, + ); + + 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)); + + // Restart the app to apply cron expression changes + if (mounted) { + Restart.restartApp(); + } + // Note: No need to reset _isSaving here since app is restarting + } else if (mounted) { + setState(() { + _isSaving = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Failed to save cron expression')), + ); + } + } catch (e) { + debugPrint('Error modifying cron expression: $e'); + if (mounted) { + setState(() { + _isSaving = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } } } @@ -178,9 +332,9 @@ class _JobCardState extends State<_JobCard> { return; } - // Refresh last and next sync time after successful sync - await _loadLastSyncTime(); - await _loadNextSyncTime(); + // Refresh last and next sync time after successful sync + if (!mounted) return; + await context.read().refreshJobStatuses(); } catch (e) { debugPrint('Sync failed for ${widget.job.id}: $e'); @@ -200,21 +354,97 @@ class _JobCardState extends State<_JobCard> { boxShadow: const [BoxShadow(color: Color(0x11000000), blurRadius: 4, offset: Offset(0, 2))], ), child: Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.all(6.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text(job.name ?? job.apiName ?? 'Unknown Job', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), - const SizedBox(height: 8), - _kv('Next Run', _nextSync ?? '-'), - _kv('Last Sync', _lastSync ?? '-'), - const SizedBox(height: 6), - _kv('Cron Expression', job.syncFreq ?? '-'), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), + Consumer( + builder: (context, provider, child) { + final status = provider.jobStatuses[job.id]; + String lastSync = status?.lastSyncTime ?? '-'; + if (widget.job.apiName == "masterSyncJob" && lastSync == "NA") { + lastSync = formatDate(provider.lastSuccessfulSyncTime); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _kv('Next Run', status?.nextSyncTime ?? '-'), + _kv('Last Sync', lastSync), + ], + ); + }, + ), + if (widget.isPermitted) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 32, + 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), + ), + ), + ], + ), + ), + const SizedBox(width: 6), + SizedBox( + height: 32, + width: 65, + child: ElevatedButton( + onPressed: _isSaving ? null : _modifyCronExpression, + style: ElevatedButton.styleFrom( + padding: EdgeInsets.zero, + ), + child: _isSaving + ? const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const Text('Submit', style: TextStyle(fontSize: 11)), + ), + ), + ], + ) + else + _kv('Cron Expression', widget.job.syncFreq ?? '-'), ], ), ), @@ -237,13 +467,16 @@ class _JobCardState extends State<_JobCard> { ); } - Widget _kv(String k, String v) => Row( - children: [ - Text(k, style: const TextStyle(fontSize: 12, color: Colors.black54)), - const SizedBox(width: 8), - Flexible( - child: Text(v, style: const TextStyle(fontSize: 12, color: Colors.black87))), - ], + Widget _kv(String k, String v) => Padding( + padding: const EdgeInsets.only(top: 0.5), + child: Row( + children: [ + Text(k, style: const TextStyle(fontSize: 11, color: Colors.black54)), + const SizedBox(width: 6), + Flexible( + child: Text(v, style: const TextStyle(fontSize: 11, color: Colors.black87))), + ], + ), ); } diff --git a/pigeon/master_data_sync.dart b/pigeon/master_data_sync.dart index 41cb9d275..315e1c47c 100644 --- a/pigeon/master_data_sync.dart +++ b/pigeon/master_data_sync.dart @@ -63,4 +63,12 @@ abstract class SyncApi { String getNextSyncTimeByJobId(String jobId); @async List getActiveSyncJobs(); + @async + List getPermittedJobs(); + @async + bool isValidCronExpression(String cronExpression); + @async + bool modifyJobCronExpression(String jobId, String cronExpression); + @async + String? getValue(String name); }