From adf6577b2bc425ea378985046cfbf39fcb333806 Mon Sep 17 00:00:00 2001 From: Madhuravas reddy Date: Fri, 30 Jan 2026 18:22:37 +0530 Subject: [PATCH 1/6] RCF-1302 implemented config properties Signed-off-by: Madhuravas reddy --- .../registration_client/MainActivity.java | 31 +- .../api_services/MasterDataSyncApi.java | 277 +++++++++++++++++- .../constant/RegistrationConstants.java | 4 + .../repository/GlobalParamRepository.java | 13 + .../service/RegistrationServiceImpl.java | 4 +- .../service/PosixAdapterServiceImpl.java | 3 - 6 files changed, 312 insertions(+), 20 deletions(-) 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 b43343ae3..be52912ed 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 @@ -24,8 +24,10 @@ import com.fasterxml.jackson.databind.ObjectWriter; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -42,6 +44,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.dto.CenterMachineDto; import io.mosip.registration.clientmanager.entity.GlobalParam; @@ -257,8 +260,15 @@ void scheduleAllActiveJobs() { try { List activeJobs = syncJobDefRepository.getAllSyncJobDefList(); int scheduledCount = 0; - + Set excludedJobIds = getExcludedJobIds(); for (SyncJobDef job : activeJobs) { + if (job.getId() == null) { + continue; + } + if (excludedJobIds.contains(job.getId())) { + Log.d(getClass().getSimpleName(), "Skipping excluded job: " + job.getId()); + continue; + } if (job.getIsActive() != null && job.getIsActive() && job.getApiName() != null) { Log.d(getClass().getSimpleName(), "Scheduling job: " + job.getApiName() + " (ID: " + job.getId() + ", Cron: " + job.getSyncFreq() + ")"); @@ -275,6 +285,25 @@ void scheduleAllActiveJobs() { }).start(); } + private Set getExcludedJobIds() { + Set excluded = new HashSet<>(); + addJobIdsFromString(excluded, globalParamRepository.getCachedStringJobsOffline()); + addJobIdsFromString(excluded, globalParamRepository.getCachedStringJobsUntagged()); + return excluded; + } + + private void addJobIdsFromString(Set target, String value) { + if (value == null || value.trim().isEmpty()) { + return; + } + for (String jobId : value.split(RegistrationConstants.COMMA)) { + String trimmed = jobId.trim(); + if (!trimmed.isEmpty()) { + target.add(trimmed); + } + } + } + @Override protected void onDestroy() { super.onDestroy(); 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 551c86628..e179babd0 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 @@ -13,11 +13,14 @@ import android.app.Activity; import android.app.AlarmManager; +import android.app.AlertDialog; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.util.Log; import android.widget.Toast; @@ -27,7 +30,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; @@ -110,6 +115,12 @@ public class MasterDataSyncApi implements MasterDataSyncPigeon.SyncApi { BatchJob batchJob; + private final Object restartLock = new Object(); + private int runningSyncJobs = 0; + private boolean restartRequested = false; + private final Handler restartHandler = new Handler(Looper.getMainLooper()); + private Runnable pendingRestartPrompt; + @Inject public MasterDataSyncApi(ClientCryptoManagerService clientCryptoManagerService, MachineRepository machineRepository, RegistrationCenterRepository registrationCenterRepository, SyncRestService syncRestService, CertificateManagerService certificateManagerService, GlobalParamRepository globalParamRepository, ObjectMapper objectMapper, UserDetailRepository userDetailRepository, IdentitySchemaRepository identitySchemaRepository, Context context, DocumentTypeRepository documentTypeRepository, ApplicantValidDocRepository applicantValidDocRepository, @@ -173,69 +184,111 @@ public void getLastSyncTime(@NonNull MasterDataSyncPigeon.Result result) { + if (isManualSync && isExcludedJob(jobId)) { + result.success(syncResult("PolicyKeySync", 5, "")); + return; + } + onSyncJobStart(); CenterMachineDto centerMachineDto = masterDataService.getRegistrationCenterMachineDetails(); if (centerMachineDto == null) { + onSyncJobComplete(jobId, false, isManualSync); result.success(syncResult("PolicyKeySync", 5, "policy_key_sync_failed")); return; } try { masterDataService.syncCertificate(() -> { - - result.success(syncResult("PolicyKeySync", 5, masterDataService.onResponseComplete())); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, isManualSync); + result.success(syncResult("PolicyKeySync", 5, errorCode)); }, REG_APP_ID, centerMachineDto.getMachineRefId(), REG_APP_ID, centerMachineDto.getMachineRefId(), isManualSync, jobId); } catch (Exception e) { e.printStackTrace(); + onSyncJobComplete(jobId, false, isManualSync); } } @Override public void getGlobalParamsSync(@NonNull Boolean isManualSync, @NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { + if (isManualSync && isExcludedJob(jobId)) { + result.success(syncResult("GlobalParamsSync", 1, "")); + return; + } + + onSyncJobStart(); try { masterDataService.syncGlobalParamsData(() -> { Log.i(TAG, "Sync Global Params Completed."); - result.success(syncResult("GlobalParamsSync", 1, masterDataService.onResponseComplete())); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, isManualSync); + result.success(syncResult("GlobalParamsSync", 1, errorCode)); }, isManualSync, jobId); } catch (Exception e) { e.printStackTrace(); + onSyncJobComplete(jobId, false, isManualSync); } } @Override public void getUserDetailsSync(@NonNull Boolean isManualSync, @NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { + if (isManualSync && isExcludedJob(jobId)) { + result.success(syncResult("UserDetailsSync", 3, "")); + return; + } + onSyncJobStart(); try { masterDataService.syncUserDetails(() -> { Log.i(TAG, "User details sync Completed."); - result.success(syncResult("UserDetailsSync", 3, masterDataService.onResponseComplete())); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, isManualSync); + result.success(syncResult("UserDetailsSync", 3, errorCode)); }, isManualSync, jobId); } catch (Exception e) { e.printStackTrace(); + onSyncJobComplete(jobId, false, isManualSync); } } @Override public void getIDSchemaSync(@NonNull Boolean isManualSync, @NonNull MasterDataSyncPigeon.Result result) { + onSyncJobStart(); try { masterDataService.syncLatestIdSchema(() -> { Log.i(TAG, "ID Schema Sync Completed"); - result.success(syncResult("LatestIDSchemaSync", 4, masterDataService.onResponseComplete())); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete("", success, isManualSync); + result.success(syncResult("LatestIDSchemaSync", 4, errorCode)); }, isManualSync); } catch (Exception e) { Log.e(TAG, "ID Schema Sync Failed.", e); e.printStackTrace(); + onSyncJobComplete("", false, isManualSync); } } @Override public void getMasterDataSync(@NonNull Boolean isManualSync, @NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { + if (isManualSync && isExcludedJob(jobId)) { + result.success(syncResult("MasterDataSync", 2, "")); + return; + } + onSyncJobStart(); try { masterDataService.syncMasterData(() -> { Log.i(TAG, "Master Data Sync Completed."); - result.success(syncResult("MasterDataSync", 2, masterDataService.onResponseComplete())); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, isManualSync); + result.success(syncResult("MasterDataSync", 2, errorCode)); }, 0, isManualSync, jobId); } catch (Exception e) { Log.e(TAG, "Master Data Sync Failed.", e); e.printStackTrace(); + onSyncJobComplete(jobId, false, isManualSync); } } @@ -250,10 +303,18 @@ private MasterDataSyncPigeon.Sync syncResult(String syncType, int progress, Stri @Override public void getCaCertsSync(@NonNull Boolean isManualSync, @NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { + if (isManualSync && isExcludedJob(jobId)) { + result.success(syncResult("CACertificatesSync", 6, "")); + return; + } + onSyncJobStart(); masterDataService.syncCACertificates(() -> { Log.i(TAG, "CA Certificate Sync Completed"); resetAlarm("registrationPacketUploadJob"); - result.success(syncResult("CACertificatesSync", 6, masterDataService.onResponseComplete())); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, isManualSync); + result.success(syncResult("CACertificatesSync", 6, errorCode)); }, isManualSync, jobId); } @@ -275,28 +336,42 @@ public void getReasonList(@NonNull String langCode, @NonNull MasterDataSyncPigeo @Override public void getPreRegIds(@NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { + onSyncJobStart(); if (NetworkUtils.isNetworkConnected(this.context)) { try { preRegistrationDataSyncService.fetchPreRegistrationIds(() -> { Log.i(TAG, "Application Id's Sync Completed"); result.success("Application Id's Sync Completed."); + onSyncJobComplete(jobId, true, false); }, jobId); } catch (Exception e) { e.printStackTrace(); + onSyncJobComplete(jobId, false, false); } + } else { + onSyncJobComplete(jobId, false, false); } } @Override public void getKernelCertsSync(@NonNull Boolean isManualSync, @NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { + if (isManualSync && isExcludedJob(jobId)) { + result.success(syncResult("KernelCertsSync", 7, "")); + return; + } + onSyncJobStart(); try { masterDataService.syncCertificate(() -> { Log.i(TAG, "Policy Key Sync Completed"); - result.success(syncResult("KernelCertsSync", 7, masterDataService.onResponseComplete())); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, isManualSync); + result.success(syncResult("KernelCertsSync", 7, errorCode)); }, KERNEL_APP_ID, "SIGN", "SERVER-RESPONSE", "SIGN-VERIFY", isManualSync, jobId); } catch (Exception e) { e.printStackTrace(); + onSyncJobComplete(jobId, false, isManualSync); } } @@ -349,6 +424,7 @@ void resetAlarm(String api) { @Override public void deleteAuditLogs(@NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { try { + onSyncJobStart(); boolean deletedRes = auditManagerService.deleteAuditLogs(); // Also persist timestamps so UI can show Last/Next immediately when triggered manually try { @@ -360,8 +436,10 @@ public void deleteAuditLogs(@NonNull String jobId, @NonNull MasterDataSyncPigeon Log.e(TAG, "Failed to store CA certificates sync last sync time", e); Toast.makeText(context, "Failed to Deleted Audit logs", Toast.LENGTH_LONG).show(); } + onSyncJobComplete(jobId, deletedRes, true); result.success(deletedRes); } catch (Exception e) { + onSyncJobComplete(jobId, false, true); result.error(e); Toast.makeText(context, "Failed to deleted Audit logs", Toast.LENGTH_LONG).show(); } @@ -370,12 +448,15 @@ public void deleteAuditLogs(@NonNull String jobId, @NonNull MasterDataSyncPigeon @Override public void deletePreRegRecords(@NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { try { + onSyncJobStart(); // Call fetchAndDeleteRecords from PreRegistrationDataSyncService preRegistrationDataSyncService.fetchAndDeleteRecords(); masterDataService.logLastSyncCompletionDateTime(jobId); Toast.makeText(context, "Deleted Pre-reg records", Toast.LENGTH_LONG).show(); + onSyncJobComplete(jobId, true, true); result.success(true); } catch (Exception e) { + onSyncJobComplete(jobId, false, true); result.error(e); Toast.makeText(context, "Failed to deleted Pre-reg records", Toast.LENGTH_LONG).show(); } @@ -384,11 +465,14 @@ public void deletePreRegRecords(@NonNull String jobId, @NonNull MasterDataSyncPi @Override public void deleteRegistrationPackets(@NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { try { + onSyncJobStart(); packetService.deleteRegistrationPackets(); masterDataService.logLastSyncCompletionDateTime(jobId); Toast.makeText(context, "Deleted Registration packets", Toast.LENGTH_LONG).show(); + onSyncJobComplete(jobId, true, true); result.success(true); } catch (Exception e) { + onSyncJobComplete(jobId, false, true); Log.e(TAG, "Failed to delete registration packets", e); result.error(e); Toast.makeText(context, "Failed to delete Registration packets", Toast.LENGTH_LONG).show(); @@ -398,11 +482,14 @@ public void deleteRegistrationPackets(@NonNull String jobId, @NonNull MasterData @Override public void syncPacketStatus(@NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { try { + onSyncJobStart(); packetService.syncAllPacketStatus(); masterDataService.logLastSyncCompletionDateTime(jobId); Log.i(TAG, "Packet status sync job completed"); + onSyncJobComplete(jobId, true, true); result.success(true); } catch (Exception e) { + onSyncJobComplete(jobId, false, true); Log.e(TAG, "Failed to sync packet status", e); result.error(e); } @@ -428,6 +515,9 @@ public void getActiveSyncJobs(@NonNull MasterDataSyncPigeon.Result> List value = new ArrayList<>(); try { for (SyncJobDef job : list) { + if (job.getId() == null) { + continue; + } value.add(objectMapper.writeValueAsString(job)); } } catch (Exception e) { @@ -507,78 +597,106 @@ public void executeJobByApiName(String jobApiName, Context context) { // Get job ID from database for tracking last/next sync String jobId = getJobIdByApiName(jobApiName); + onSyncJobStart(); // Execute appropriate sync job switch (jobApiName) { case "registrationPacketUploadJob": batchJob.syncRegistrationPackets(context); + onSyncJobComplete(jobId, false, false); break; case "packetSyncStatusJob": packetService.syncAllPacketStatus(); + onSyncJobComplete(jobId, true, false); break; case "masterSyncJob": masterDataService.syncMasterData(() -> { Log.d(getClass().getSimpleName(), "Master data sync callback"); - }, 0, true, jobId); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, false); + }, 0, false, jobId); break; case "synchConfigDataJob": masterDataService.syncGlobalParamsData(() -> { Log.d(getClass().getSimpleName(), "Config data sync callback"); - }, true, jobId); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, false); + }, false, jobId); break; case "userDetailServiceJob": masterDataService.syncUserDetails(() -> { Log.d(getClass().getSimpleName(), "User details sync callback"); - }, true, jobId); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, false); + }, false, jobId); break; case "keyPolicySyncJob": CenterMachineDto centerMachineDto = masterDataService.getRegistrationCenterMachineDetails(); if (centerMachineDto != null && centerMachineDto.getMachineRefId() != null) { masterDataService.syncCertificate(() -> { Log.d(getClass().getSimpleName(), "Policy key sync callback"); - }, REG_APP_ID, centerMachineDto.getMachineRefId(), REG_APP_ID, centerMachineDto.getMachineRefId(), true, jobId); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, false); + }, REG_APP_ID, centerMachineDto.getMachineRefId(), REG_APP_ID, centerMachineDto.getMachineRefId(), false, jobId); } else { Log.w(getClass().getSimpleName(), "Skipping keyPolicySyncJob - machine details not available"); + onSyncJobComplete(jobId, false, false); } break; case "publicKeySyncJob": // Public key sync for KERNEL app (SIGN certificates) masterDataService.syncCertificate(() -> { Log.d(getClass().getSimpleName(), "Public key sync callback"); - }, KERNEL_APP_ID, "SIGN", "SERVER-RESPONSE", "SIGN-VERIFY", true, jobId); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, false); + }, KERNEL_APP_ID, "SIGN", "SERVER-RESPONSE", "SIGN-VERIFY", false, jobId); break; case "syncCertificateJob": // CA certificate sync masterDataService.syncCACertificates(() -> { Log.d(getClass().getSimpleName(), "CA cert sync callback"); - }, true, jobId); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, false); + }, false, jobId); break; case "preRegistrationDataSyncJob": preRegistrationDataSyncService.fetchPreRegistrationIds(() -> { Log.i(TAG, "Application Id's Sync Completed"); + onSyncJobComplete(jobId, true, false); }, jobId); break; case "deleteAuditLogsJob": auditManagerService.deleteAuditLogs(); masterDataService.logLastSyncCompletionDateTime(jobId); + onSyncJobComplete(jobId, true, false); break; case "preRegistrationPacketDeletionJob": preRegistrationDataSyncService.fetchAndDeleteRecords(); masterDataService.logLastSyncCompletionDateTime(jobId); + onSyncJobComplete(jobId, true, false); break; case "registrationDeletionJob": packetService.deleteRegistrationPackets(); masterDataService.logLastSyncCompletionDateTime(jobId); Log.i(TAG, "Registration packet deletion job completed"); + onSyncJobComplete(jobId, true, false); break; default: Log.w(getClass().getSimpleName(), "Unknown job: " + jobApiName); + onSyncJobComplete(jobId, false, false); } Log.d(getClass().getSimpleName(), "Completed: " + jobApiName); } catch (Exception e) { + onSyncJobComplete(getJobIdByApiName(jobApiName), false, false); Log.e(getClass().getSimpleName(), "Job failed: " + jobApiName, e); } }).start(); @@ -595,4 +713,135 @@ private String getJobIdByApiName(String apiName) { } return ""; // Return empty string if not found } + + private Set getExcludedJobIds() { + Set excluded = new HashSet<>(); + addJobIdsFromString(excluded, globalParamRepository.getCachedStringJobsOffline()); + addJobIdsFromString(excluded, globalParamRepository.getCachedStringJobsUntagged()); + return excluded; + } + + private void addJobIdsFromString(Set target, String value) { + if (value == null || value.trim().isEmpty()) { + return; + } + for (String jobId : value.split(RegistrationConstants.COMMA)) { + String trimmed = jobId.trim(); + if (!trimmed.isEmpty()) { + target.add(trimmed); + } + } + } + + /** True if job is in offline or untagged list (excluded from auto sync and manual sync execution). */ + private boolean isExcludedJob(String jobId) { + return jobId != null && !jobId.trim().isEmpty() && getExcludedJobIds().contains(jobId.trim()); + } + + private void onSyncJobStart() { + synchronized (restartLock) { + runningSyncJobs++; + } + cancelPendingRestartPrompt(); + } + + private void onSyncJobComplete(String jobId, boolean success, boolean isManualSync) { + boolean shouldPrompt = false; + synchronized (restartLock) { + if (runningSyncJobs > 0) { + runningSyncJobs--; + } + // Only request restart when a restartable job completed successfully during manual sync + if (success && isRestartableJob(jobId) && isManualSync) { + restartRequested = true; + } + if (runningSyncJobs == 0 && restartRequested) { + shouldPrompt = true; + } + } + if (shouldPrompt) { + scheduleRestartPrompt(); + } + } + + private boolean isRestartableJob(String jobId) { + if (jobId == null || jobId.trim().isEmpty()) { + return false; + } + String value = globalParamRepository.getCachedStringJobsRestart(); + if (value == null || value.trim().isEmpty()) { + return false; + } + String target = jobId.trim(); + for (String id : value.split(RegistrationConstants.COMMA)) { + if (target.equals(id.trim())) { + return true; + } + } + return false; + } + + private void scheduleRestartPrompt() { + cancelPendingRestartPrompt(); + pendingRestartPrompt = () -> { + boolean showPrompt = false; + synchronized (restartLock) { + if (runningSyncJobs == 0 && restartRequested) { + restartRequested = false; + showPrompt = true; + } + } + if (showPrompt) { + showRestartDialog(); + } + }; + // Delay to allow any queued syncs to start + restartHandler.postDelayed(pendingRestartPrompt, 2000); + } + + private void cancelPendingRestartPrompt() { + if (pendingRestartPrompt != null) { + restartHandler.removeCallbacks(pendingRestartPrompt); + pendingRestartPrompt = null; + } + } + + private void showRestartDialog() { + if (activity == null || activity.isFinishing()) { + Log.w(TAG, "Restart prompt skipped: activity not available"); + return; + } + activity.runOnUiThread(() -> { + try { + new AlertDialog.Builder(activity, android.R.style.Theme_Material_Light_Dialog_Alert) + .setTitle("Sync Completed Successfully") + .setMessage("The app will restart once you click Restart, and you will be redirected to the login page.") + .setCancelable(false) + .setPositiveButton("Restart", (dialog, which) -> requestAppRestart()) + .show(); + } catch (Exception e) { + Log.e(TAG, "Failed to show restart dialog", e); + } + }); + } + + private void requestAppRestart() { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> { + try { + Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); + if (intent != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + context.startActivity(intent); + } + if (activity != null && !activity.isFinishing()) { + activity.finishAffinity(); + } + // Do not call Runtime.getRuntime().exit(0) - it kills the process before + // the new activity can start; finishing affinity allows a clean restart. + } catch (Exception e) { + Log.e(TAG, "Failed to restart application", e); + } + }); + } } diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/constant/RegistrationConstants.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/constant/RegistrationConstants.java index 966fd27d9..7f4ea26ba 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/constant/RegistrationConstants.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/constant/RegistrationConstants.java @@ -153,6 +153,10 @@ public class RegistrationConstants { public static final String REG_PAK_MAX_CNT_APPRV_LIMIT = "mosip.registration.reg_pak_max_cnt_apprv_limit"; public static final String PACKET_STORE_LOCATION = "mosip.registration.registration_packet_store_location"; + public static final String JOBS_OFFLINE = "mosip.registration.jobs.offline"; + public static final String JOBS_UNTAGGED = "mosip.registration.jobs.unTagged"; + public static final String JOBS_RESTART = "mosip.registration.jobs.restart"; + // Sync status validation constants public static final String MOSIP_REGISTRATION = "mosip.registration."; public static final String DOT = "."; diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/GlobalParamRepository.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/GlobalParamRepository.java index 01470f3c6..008097c11 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/GlobalParamRepository.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/repository/GlobalParamRepository.java @@ -254,10 +254,23 @@ public String getCachedStringFieldsToRetainOnPridFetch(){ public int getCachedIntRegMaxCountApproveLimit(){ return getCachedIntegerGlobalParam(RegistrationConstants.REG_PAK_MAX_CNT_APPRV_LIMIT); } + public String getCachedStringPacketStoreLocation() { return globalParamMap.get(RegistrationConstants.PACKET_STORE_LOCATION); } + public String getCachedStringJobsOffline() { + return globalParamMap.get(RegistrationConstants.JOBS_OFFLINE); + } + + public String getCachedStringJobsUntagged() { + return globalParamMap.get(RegistrationConstants.JOBS_UNTAGGED); + } + + public String getCachedStringJobsRestart() { + return globalParamMap.get(RegistrationConstants.JOBS_RESTART); + } + /** * Refresh configuration cache by merging global params with local preferences */ diff --git a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/RegistrationServiceImpl.java b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/RegistrationServiceImpl.java index 3b33c4983..e1244a411 100644 --- a/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/RegistrationServiceImpl.java +++ b/android/clientmanager/src/main/java/io/mosip/registration/clientmanager/service/RegistrationServiceImpl.java @@ -574,7 +574,7 @@ public List> getAudits() { private void doPreChecksBeforeRegistration(CenterMachineDto centerMachineDto) throws Exception { //free space validation - if (validatingDiskSpace()) { + if (isDiskSpaceAvailable()) { throw new ClientCheckedException("PAK_DISK_SPACE_LOW"); } @@ -606,7 +606,7 @@ private void doPreChecksBeforeRegistration(CenterMachineDto centerMachineDto) th } } - private boolean validatingDiskSpace() { + private boolean isDiskSpaceAvailable() { int minSpaceRequiredMB = globalParamRepository.getCachedIntegerDiskSpaceSize(); if (minSpaceRequiredMB <= 0) { minSpaceRequiredMB = DEFAULT_MIN_SPACE_REQUIRED_MB; diff --git a/android/packetmanager/src/main/java/io/mosip/registration/packetmanager/service/PosixAdapterServiceImpl.java b/android/packetmanager/src/main/java/io/mosip/registration/packetmanager/service/PosixAdapterServiceImpl.java index 4fcafc2ba..26963464e 100644 --- a/android/packetmanager/src/main/java/io/mosip/registration/packetmanager/service/PosixAdapterServiceImpl.java +++ b/android/packetmanager/src/main/java/io/mosip/registration/packetmanager/service/PosixAdapterServiceImpl.java @@ -75,9 +75,6 @@ private void initPosixAdapterService(Context context) { File baseDir = StorageUtils.getPacketStorageDir(context); if (baseDir.exists() || baseDir.mkdirs()) { BASE_LOCATION = baseDir.getAbsolutePath(); - } else { - Log.e(TAG, "Failed to initialize packet storage directory: " + baseDir.getAbsolutePath()); - BASE_LOCATION = null; } } From 4fb3c59deb89fd64137f74c0dbeac95a6e2c622c Mon Sep 17 00:00:00 2001 From: Madhuravas reddy Date: Mon, 9 Feb 2026 14:15:54 +0530 Subject: [PATCH 2/6] Resolved code rabbit review comments Signed-off-by: Madhuravas reddy --- .../api_services/MasterDataSyncApi.java | 19 +++++++---- .../registration_client/utils/BatchJob.java | 32 ++++++++++++++++--- .../sync_response_service_impl.dart | 4 +-- lib/platform_spi/sync_response_service.dart | 2 +- lib/provider/sync_provider.dart | 4 +-- pigeon/master_data_sync.dart | 2 +- 6 files changed, 47 insertions(+), 16 deletions(-) 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 e179babd0..c37774baf 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 @@ -253,20 +253,25 @@ public void getUserDetailsSync(@NonNull Boolean isManualSync, @NonNull String jo } @Override - public void getIDSchemaSync(@NonNull Boolean isManualSync, @NonNull MasterDataSyncPigeon.Result result) { + public void getIDSchemaSync(@NonNull Boolean isManualSync, @NonNull String jobId, @NonNull MasterDataSyncPigeon.Result result) { + if (isManualSync && isExcludedJob(jobId)) { + result.success(syncResult("LatestIDSchemaSync", 4, "")); + return; + } onSyncJobStart(); try { masterDataService.syncLatestIdSchema(() -> { Log.i(TAG, "ID Schema Sync Completed"); String errorCode = masterDataService.onResponseComplete(); boolean success = errorCode == null || errorCode.isEmpty(); - onSyncJobComplete("", success, isManualSync); + onSyncJobComplete(jobId, success, isManualSync); result.success(syncResult("LatestIDSchemaSync", 4, errorCode)); }, isManualSync); } catch (Exception e) { Log.e(TAG, "ID Schema Sync Failed.", e); e.printStackTrace(); - onSyncJobComplete("", false, isManualSync); + onSyncJobComplete(jobId, false, isManualSync); + result.error(e); } } @@ -320,7 +325,7 @@ public void getCaCertsSync(@NonNull Boolean isManualSync, @NonNull String jobId, @Override public void batchJob(@NonNull MasterDataSyncPigeon.Result result) { - batchJob.syncRegistrationPackets(this.context); + batchJob.syncRegistrationPackets(this.context, null); result.success("Registration Packet Sync Completed."); } @@ -602,8 +607,10 @@ public void executeJobByApiName(String jobApiName, Context context) { // Execute appropriate sync job switch (jobApiName) { case "registrationPacketUploadJob": - batchJob.syncRegistrationPackets(context); - onSyncJobComplete(jobId, false, false); + batchJob.syncRegistrationPackets(context, () -> { + Log.d(getClass().getSimpleName(), "Registration packet upload job completed"); + onSyncJobComplete(jobId, true, false); + }); break; case "packetSyncStatusJob": packetService.syncAllPacketStatus(); 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 3c60a4c58..2f63aa4aa 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 @@ -71,7 +71,7 @@ private List getRegistrationList(List statusList) { return registrationList; } - public void syncRegistrationPackets(Context context) { + public void syncRegistrationPackets(Context context, Runnable onComplete) { Log.d(getClass().getSimpleName(), "Sync Packets in Batch Job"); List registrationList = getRegistrationList(Arrays.asList(PacketClientStatus.APPROVED.name(), PacketClientStatus.REJECTED.name())); final Integer[] remainingPack = {registrationList.size(), 0}; @@ -80,7 +80,7 @@ public void syncRegistrationPackets(Context context) { CustomToast newToast = new CustomToast(activity); if (registrationList.isEmpty()) { - uploadRegistrationPackets(context); + uploadRegistrationPackets(context, onComplete); return; } for (Registration value : registrationList) { @@ -125,18 +125,25 @@ public void onComplete(String RID, PacketTaskStatus status) { newToast.showToast(); Log.d(getClass().getSimpleName(), "Last Packet" + RID); - uploadRegistrationPackets(context); + uploadRegistrationPackets(context, onComplete); } } }); } catch (Exception e) { syncAndUploadInProgressStatus = false; Log.e(getClass().getSimpleName(), e.getMessage()); + // If exception occurs and no more packets remain, call completion callback + remainingPack[0] -= 1; + if (remainingPack[0] == 0 && onComplete != null) { + uploadRegistrationPackets(context, onComplete); + } else if (remainingPack[0] == 0) { + uploadRegistrationPackets(context, null); + } } } } - public void uploadRegistrationPackets(Context context) { + public void uploadRegistrationPackets(Context context, Runnable onComplete) { Log.d(getClass().getSimpleName(), "Upload Packets in Batch Job"); List registrationList = getRegistrationList(Arrays.asList(PacketClientStatus.SYNCED.name(), PacketClientStatus.EXPORTED.name())); @@ -144,6 +151,14 @@ public void uploadRegistrationPackets(Context context) { final Integer[] remainingPack = {packetSize, 0}; CustomToast newToast = new CustomToast(activity); + if (registrationList.isEmpty()) { + // If no packets to upload, call completion callback if provided + if (onComplete != null) { + onComplete.run(); + } + return; + } + for (Registration value : registrationList) { try { syncAndUploadInProgressStatus = true; @@ -184,12 +199,21 @@ public void onComplete(String RID, PacketTaskStatus status) { } newToast.setText(message); newToast.showToast(); + + // Call completion callback when all uploads finish + if (onComplete != null) { + onComplete.run(); + } } } }); } catch (Exception e) { syncAndUploadInProgressStatus = false; Log.e(getClass().getSimpleName(), e.getMessage()); + // Call completion callback even on error to prevent hanging + if (onComplete != null) { + onComplete.run(); + } } } } diff --git a/lib/platform_android/sync_response_service_impl.dart b/lib/platform_android/sync_response_service_impl.dart index d33ab8227..0a58f4198 100644 --- a/lib/platform_android/sync_response_service_impl.dart +++ b/lib/platform_android/sync_response_service_impl.dart @@ -52,10 +52,10 @@ class SyncResponseServiceImpl implements SyncResponseService { } @override - Future getIDSchemaSync(bool isManualSync) async { + Future getIDSchemaSync(bool isManualSync, String jobId) async { late Sync syncResponse; try { - syncResponse = await SyncApi().getIDSchemaSync(isManualSync); + syncResponse = await SyncApi().getIDSchemaSync(isManualSync, jobId); } on PlatformException { debugPrint('IDSchemaSync Api call failed, PlatformException'); } catch (e) { diff --git a/lib/platform_spi/sync_response_service.dart b/lib/platform_spi/sync_response_service.dart index 872cab2f6..0d5bbf8d7 100644 --- a/lib/platform_spi/sync_response_service.dart +++ b/lib/platform_spi/sync_response_service.dart @@ -13,7 +13,7 @@ abstract class SyncResponseService { Future getPolicyKeySync(bool isManualSync, String jobId); Future getGlobalParamsSync(bool isManualSync, String jobId); Future getUserDetailsSync(bool isManualSync, String jobId); - Future getIDSchemaSync(bool isManualSync); + Future getIDSchemaSync(bool isManualSync, String jobId); Future getMasterDataSync(bool isManualSync, String jobId); Future getCaCertsSync(bool isManualSync, String jobId); Future batchJob(); diff --git a/lib/provider/sync_provider.dart b/lib/provider/sync_provider.dart index f7b14ea80..65ba4390e 100644 --- a/lib/provider/sync_provider.dart +++ b/lib/provider/sync_provider.dart @@ -163,7 +163,7 @@ class SyncProvider with ChangeNotifier { }); await syncResponseService - .getIDSchemaSync(false) + .getIDSchemaSync(false, findJobIdByApiName("latestIdSchemaSyncJob")) .then((Sync getAutoSync) async { setCurrentProgressType(getAutoSync.syncType!); if (getAutoSync.errorCode == "") { @@ -237,7 +237,7 @@ class SyncProvider with ChangeNotifier { Sync syncResult = await syncResponseService.getMasterDataSync(true, findJobIdByApiName("masterSyncJob")); if (syncResult.errorCode != null && syncResult.errorCode!.isEmpty) { - syncResult = await syncResponseService.getIDSchemaSync(true); + syncResult = await syncResponseService.getIDSchemaSync(true, findJobIdByApiName("latestIdSchemaSyncJob")); if (syncResult.errorCode != null && syncResult.errorCode!.isEmpty) { syncResult = await syncResponseService.getUserDetailsSync(true, findJobIdByApiName("userDetailServiceJob")); if (syncResult.errorCode != null && syncResult.errorCode!.isEmpty) { diff --git a/pigeon/master_data_sync.dart b/pigeon/master_data_sync.dart index 315e1c47c..563ab2252 100644 --- a/pigeon/master_data_sync.dart +++ b/pigeon/master_data_sync.dart @@ -29,7 +29,7 @@ abstract class SyncApi { Sync getUserDetailsSync(bool isManualSync, String jobId); @async - Sync getIDSchemaSync(bool isManualSync); + Sync getIDSchemaSync(bool isManualSync, String jobId); @async Sync getMasterDataSync(bool isManualSync, String jobId); From 7ffe203ba6b312884aa45a2c680b391c3209dea1 Mon Sep 17 00:00:00 2001 From: Madhuravas reddy Date: Mon, 9 Feb 2026 15:19:48 +0530 Subject: [PATCH 3/6] Resolved code rabiit review comments Signed-off-by: Madhuravas reddy --- .../api_services/MasterDataSyncApi.java | 23 +++++++---- .../registration_client/utils/BatchJob.java | 40 ++++++++++++++----- 2 files changed, 46 insertions(+), 17 deletions(-) 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 c37774baf..3e820bdc6 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 @@ -313,14 +313,21 @@ public void getCaCertsSync(@NonNull Boolean isManualSync, @NonNull String jobId, return; } onSyncJobStart(); - masterDataService.syncCACertificates(() -> { - Log.i(TAG, "CA Certificate Sync Completed"); - resetAlarm("registrationPacketUploadJob"); - String errorCode = masterDataService.onResponseComplete(); - boolean success = errorCode == null || errorCode.isEmpty(); - onSyncJobComplete(jobId, success, isManualSync); - result.success(syncResult("CACertificatesSync", 6, errorCode)); - }, isManualSync, jobId); + try { + masterDataService.syncCACertificates(() -> { + Log.i(TAG, "CA Certificate Sync Completed"); + resetAlarm("registrationPacketUploadJob"); + String errorCode = masterDataService.onResponseComplete(); + boolean success = errorCode == null || errorCode.isEmpty(); + onSyncJobComplete(jobId, success, isManualSync); + result.success(syncResult("CACertificatesSync", 6, errorCode)); + }, isManualSync, jobId); + } catch (Exception e) { + Log.e(TAG, "CA Certificate Sync Failed.", e); + e.printStackTrace(); + onSyncJobComplete(jobId, false, isManualSync); + result.error(e); + } } @Override 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 2f63aa4aa..36118fedc 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 @@ -130,14 +130,18 @@ public void onComplete(String RID, PacketTaskStatus status) { } }); } catch (Exception e) { - syncAndUploadInProgressStatus = false; Log.e(getClass().getSimpleName(), e.getMessage()); - // If exception occurs and no more packets remain, call completion callback + // If exception occurs, decrement counter and check if all packets are done remainingPack[0] -= 1; - if (remainingPack[0] == 0 && onComplete != null) { - uploadRegistrationPackets(context, onComplete); - } else if (remainingPack[0] == 0) { - uploadRegistrationPackets(context, null); + if (remainingPack[0] == 0) { + // All packets processed (either completed or failed) + syncAndUploadInProgressStatus = false; + Log.d(getClass().getSimpleName(), "Last Packet (exception)"); + if (onComplete != null) { + uploadRegistrationPackets(context, onComplete); + } else { + uploadRegistrationPackets(context, null); + } } } } @@ -210,9 +214,27 @@ public void onComplete(String RID, PacketTaskStatus status) { } catch (Exception e) { syncAndUploadInProgressStatus = false; Log.e(getClass().getSimpleName(), e.getMessage()); - // Call completion callback even on error to prevent hanging - if (onComplete != null) { - onComplete.run(); + // If exception occurs, decrement counter and check if all packets are done + remainingPack[0] -= 1; + if (remainingPack[0] == 0) { + // All packets processed (either completed or failed) + syncAndUploadInProgressStatus = false; + Integer failed = packetSize - remainingPack[1]; + newToast.setIcon(R.drawable.done); + String message = "Upload Packet Status :"; + if (remainingPack[1] != 0) { + message = message + String.format(" %s/%s Success", remainingPack[1], packetSize); + } + if (failed != 0) { + message = message + String.format(" %s/%s Failed", failed, packetSize); + } + newToast.setText(message); + newToast.showToast(); + + // Call completion callback when all uploads finish + if (onComplete != null) { + onComplete.run(); + } } } } From fea1665c35b3b552ca8bf9e72d472c04b619473f Mon Sep 17 00:00:00 2001 From: Madhuravas reddy Date: Mon, 9 Feb 2026 15:24:57 +0530 Subject: [PATCH 4/6] Resolved code rabiit review comments Signed-off-by: Madhuravas reddy --- .../api_services/MasterDataSyncApi.java | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) 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 3e820bdc6..a29f1ec32 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 @@ -796,24 +796,36 @@ private boolean isRestartableJob(String jobId) { } private void scheduleRestartPrompt() { - cancelPendingRestartPrompt(); - pendingRestartPrompt = () -> { - boolean showPrompt = false; - synchronized (restartLock) { - if (runningSyncJobs == 0 && restartRequested) { - restartRequested = false; - showPrompt = true; + synchronized (restartLock) { + // Ensure any previously scheduled prompt is cancelled before scheduling a new one + cancelPendingRestartPromptLocked(); + pendingRestartPrompt = () -> { + boolean showPrompt = false; + synchronized (restartLock) { + if (runningSyncJobs == 0 && restartRequested) { + restartRequested = false; + showPrompt = true; + } } - } - if (showPrompt) { - showRestartDialog(); - } - }; - // Delay to allow any queued syncs to start - restartHandler.postDelayed(pendingRestartPrompt, 2000); + if (showPrompt) { + showRestartDialog(); + } + }; + // Delay to allow any queued syncs to start + restartHandler.postDelayed(pendingRestartPrompt, 2000); + } } private void cancelPendingRestartPrompt() { + synchronized (restartLock) { + cancelPendingRestartPromptLocked(); + } + } + + /** + * Internal helper that assumes the caller already holds restartLock. + */ + private void cancelPendingRestartPromptLocked() { if (pendingRestartPrompt != null) { restartHandler.removeCallbacks(pendingRestartPrompt); pendingRestartPrompt = null; From 24453ef097041a192cd48472d99e8e249f45646f Mon Sep 17 00:00:00 2001 From: Madhuravas reddy Date: Mon, 23 Feb 2026 17:38:30 +0530 Subject: [PATCH 5/6] showing the popup from UI side with multi language labels Signed-off-by: Madhuravas reddy --- .../registration_client/MainActivity.java | 2 +- .../api_services/MasterDataSyncApi.java | 75 +++++++------------ assets/l10n/app_ar.arb | 4 +- assets/l10n/app_en.arb | 4 +- assets/l10n/app_fr.arb | 4 +- assets/l10n/app_hi.arb | 4 +- assets/l10n/app_kn.arb | 4 +- assets/l10n/app_ta.arb | 4 +- lib/main.dart | 44 +++++++++++ 9 files changed, 90 insertions(+), 55 deletions(-) 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 82ace4700..333d25979 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 @@ -409,7 +409,7 @@ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { DynamicResponsePigeon.DynamicResponseApi.setup(flutterEngine.getDartExecutor().getBinaryMessenger(), dynamicDetailsApi); batchJob.setCallbackActivity(this); MasterDataSyncPigeon.SyncApi.setup(flutterEngine.getDartExecutor().getBinaryMessenger(), masterDataSyncApi); - masterDataSyncApi.setCallbackActivity(this, batchJob); + masterDataSyncApi.setCallbackActivity(this, batchJob, flutterEngine.getDartExecutor().getBinaryMessenger()); AuditResponsePigeon.AuditResponseApi.setup(flutterEngine.getDartExecutor().getBinaryMessenger(), auditDetailsApi); GlobalConfigSettingsPigeon.GlobalConfigSettingsApi.setup(flutterEngine.getDartExecutor().getBinaryMessenger(), globalConfigSettingsApi); } 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 009f87ea1..06e5a3e53 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 @@ -13,7 +13,6 @@ import android.app.Activity; import android.app.AlarmManager; -import android.app.AlertDialog; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -33,6 +32,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import javax.inject.Inject; import javax.inject.Singleton; @@ -75,6 +76,8 @@ import io.mosip.registration_client.utils.BatchJob; import io.mosip.registration_client.MainActivity; import io.mosip.registration_client.UploadBackgroundService; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; import io.mosip.registration_client.model.MasterDataSyncPigeon; import io.mosip.registration_client.utils.NetworkUtils; import io.mosip.registration.clientmanager.constant.AuditEvent; @@ -116,12 +119,13 @@ public class MasterDataSyncApi implements MasterDataSyncPigeon.SyncApi { private Activity activity; BatchJob batchJob; + private BinaryMessenger flutterBinaryMessenger; private final Object restartLock = new Object(); - private int runningSyncJobs = 0; - private boolean restartRequested = false; + private final AtomicInteger runningSyncJobs = new AtomicInteger(0); + private final AtomicBoolean restartRequested = new AtomicBoolean(false); private final Handler restartHandler = new Handler(Looper.getMainLooper()); - private Runnable pendingRestartPrompt; + private volatile Runnable pendingRestartPrompt; @Inject public MasterDataSyncApi(ClientCryptoManagerService clientCryptoManagerService, MachineRepository machineRepository, RegistrationCenterRepository registrationCenterRepository, SyncRestService syncRestService, CertificateManagerService certificateManagerService, GlobalParamRepository globalParamRepository, ObjectMapper objectMapper, UserDetailRepository userDetailRepository, IdentitySchemaRepository identitySchemaRepository, Context context, DocumentTypeRepository documentTypeRepository, @@ -165,9 +169,10 @@ public MasterDataSyncApi(ClientCryptoManagerService clientCryptoManagerService, this.localConfigService = localConfigService; } - public void setCallbackActivity(MainActivity mainActivity, BatchJob batchJob) { + public void setCallbackActivity(MainActivity mainActivity, BatchJob batchJob, BinaryMessenger flutterBinaryMessenger) { this.activity = mainActivity; this.batchJob = batchJob; + this.flutterBinaryMessenger = flutterBinaryMessenger; } @Override @@ -765,23 +770,21 @@ private boolean isExcludedJob(String jobId) { } private void onSyncJobStart() { - synchronized (restartLock) { - runningSyncJobs++; - } + runningSyncJobs.incrementAndGet(); cancelPendingRestartPrompt(); } private void onSyncJobComplete(String jobId, boolean success, boolean isManualSync) { boolean shouldPrompt = false; synchronized (restartLock) { - if (runningSyncJobs > 0) { - runningSyncJobs--; + if (runningSyncJobs.get() > 0) { + runningSyncJobs.decrementAndGet(); } // Only request restart when a restartable job completed successfully during manual sync if (success && isRestartableJob(jobId) && isManualSync) { - restartRequested = true; + restartRequested.set(true); } - if (runningSyncJobs == 0 && restartRequested) { + if (runningSyncJobs.get() == 0 && restartRequested.get()) { shouldPrompt = true; } } @@ -814,8 +817,8 @@ private void scheduleRestartPrompt() { pendingRestartPrompt = () -> { boolean showPrompt = false; synchronized (restartLock) { - if (runningSyncJobs == 0 && restartRequested) { - restartRequested = false; + if (runningSyncJobs.get() == 0 && restartRequested.get()) { + restartRequested.set(false); showPrompt = true; } } @@ -844,42 +847,18 @@ private void cancelPendingRestartPromptLocked() { } } + private static final String SYNC_RESTART_CHANNEL = "io.mosip.registration_client/sync_restart"; + private void showRestartDialog() { - if (activity == null || activity.isFinishing()) { - Log.w(TAG, "Restart prompt skipped: activity not available"); + if (flutterBinaryMessenger == null) { + Log.w(TAG, "Restart prompt skipped: Flutter binary messenger not set"); return; } - activity.runOnUiThread(() -> { - try { - new AlertDialog.Builder(activity, android.R.style.Theme_Material_Light_Dialog_Alert) - .setTitle("Sync Completed Successfully") - .setMessage("The app will restart once you click Restart, and you will be redirected to the login page.") - .setCancelable(false) - .setPositiveButton("Restart", (dialog, which) -> requestAppRestart()) - .show(); - } catch (Exception e) { - Log.e(TAG, "Failed to show restart dialog", e); - } - }); - } - - private void requestAppRestart() { - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(() -> { - try { - Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); - if (intent != null) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - context.startActivity(intent); - } - if (activity != null && !activity.isFinishing()) { - activity.finishAffinity(); - } - // Do not call Runtime.getRuntime().exit(0) - it kills the process before - // the new activity can start; finishing affinity allows a clean restart. - } catch (Exception e) { - Log.e(TAG, "Failed to restart application", e); - } - }); + try { + new MethodChannel(flutterBinaryMessenger, SYNC_RESTART_CHANNEL) + .invokeMethod("showRestartDialog", null); + } catch (Exception e) { + Log.e(TAG, "Failed to request Flutter restart dialog", e); + } } } diff --git a/assets/l10n/app_ar.arb b/assets/l10n/app_ar.arb index 8e3f85415..fb86f7ee9 100644 --- a/assets/l10n/app_ar.arb +++ b/assets/l10n/app_ar.arb @@ -358,6 +358,8 @@ "update_officer_biometric": "تحديث التحقق البيومتري للضابط", "submit_changes": "إرسال التغييرات", "local_preferences_saved_msg": "تم حفظ التفضيلات المحلية بنجاح. يلزم إعادة تشغيل التطبيق لرؤية التغييرات. هل ترغب في الإنهاء الآن؟", - "search_for_key": "ابحث عن المفتاح" + "search_for_key": "ابحث عن المفتاح", + "sync_restart_dialog_message": "سيتم إعادة تشغيل التطبيق بمجرد النقر على إعادة التشغيل، وسيتم توجيهك إلى صفحة تسجيل الدخول.", + "sync_restart_button": "إعادة التشغيل" } diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb index aaa4b15f6..ae19f77ab 100644 --- a/assets/l10n/app_en.arb +++ b/assets/l10n/app_en.arb @@ -358,5 +358,7 @@ "update_officer_biometric": "Officer's Biometric Update", "submit_changes": "Submit Changes", "local_preferences_saved_msg": "Local Preferences have been saved successfully. Application restart is needed to see the changes. Do you want to quit now?", - "search_for_key": "Search for Key" + "search_for_key": "Search for Key", + "sync_restart_dialog_message": "The app will restart once you click Restart, and you will be redirected to the login page.", + "sync_restart_button": "Restart" } diff --git a/assets/l10n/app_fr.arb b/assets/l10n/app_fr.arb index 58854ab8a..6c67cc89a 100644 --- a/assets/l10n/app_fr.arb +++ b/assets/l10n/app_fr.arb @@ -358,6 +358,8 @@ "update_officer_biometric": "Mise à jour biométrique de l’officier", "submit_changes": "Soumettre les modifications", "local_preferences_saved_msg": "Les préférences locales ont été enregistrées avec succès. Le redémarrage de l'application est nécessaire pour voir les changements. Voulez-vous quitter maintenant ?", - "search_for_key": "Rechercher une clé" + "search_for_key": "Rechercher une clé", + "sync_restart_dialog_message": "L'application redémarrera une fois que vous aurez cliqué sur Redémarrer, et vous serez redirigé vers la page de connexion.", + "sync_restart_button": "Redémarrer" } diff --git a/assets/l10n/app_hi.arb b/assets/l10n/app_hi.arb index 8bfff36b7..999df4a23 100644 --- a/assets/l10n/app_hi.arb +++ b/assets/l10n/app_hi.arb @@ -358,5 +358,7 @@ "update_officer_biometric": "अधिकारी का बायोमेट्रिक अपडेट", "submit_changes": "परिवर्तन जमा करें", "local_preferences_saved_msg": "स्थानीय प्राथमिकताएँ सफलतापूर्वक सहेज ली गई हैं। बदलाव देखने के लिए एप्लिकेशन को पुनः शुरू करना आवश्यक है। क्या आप अभी बाहर निकलना चाहते हैं?", - "search_for_key": "कुंजी खोजें" + "search_for_key": "कुंजी खोजें", + "sync_restart_dialog_message": "जैसे ही आप पुनः आरंभ करें पर क्लिक करेंगे ऐप पुनः आरंभ हो जाएगा, और आपको लॉगिन पृष्ठ पर पुनः निर्देशित किया जाएगा।", + "sync_restart_button": "पुनः आरंभ करें" } diff --git a/assets/l10n/app_kn.arb b/assets/l10n/app_kn.arb index 3349896cc..de20aa5d3 100644 --- a/assets/l10n/app_kn.arb +++ b/assets/l10n/app_kn.arb @@ -358,5 +358,7 @@ "update_officer_biometric": "ಅಧಿಕಾರಿಯ ಬಯೋಮೆಟ್ರಿಕ್ ನವೀಕರಣ", "submit_changes": "ಬದಲಾವಣೆಗಳನ್ನು ಸಲ್ಲಿಸಿ", "local_preferences_saved_msg": "ಸ್ಥಳೀಯ ಆದ್ಯತೆಗಳನ್ನು ಯಶಸ್ವಿಯಾಗಿ ಸಂಗ್ರಹಿಸಲಾಗಿದೆ. ಬದಲಾವಣೆಗಳನ್ನು ನೋಡಲು ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕು. ನೀವು ಈಗ ನಿರ್ಗಮಿಸಲು ಬಯಸುವಿರಾ?", - "search_for_key": "ಕೀಗಾಗಿ ಹುಡುಕಿ" + "search_for_key": "ಕೀಗಾಗಿ ಹುಡುಕಿ", + "sync_restart_dialog_message": "ನೀವು ಮರುಪ್ರಾರಂಭ ಕ್ಲಿಕ್ ಮಾಡಿದ ನಂತರ ಅಪ್ಲಿಕೇಶನ್ ಮರುಪ್ರಾರಂಭವಾಗುತ್ತದೆ ಮತ್ತು ನಿಮ್ಮನ್ನು ಲಾಗಿನ್ ಪುಟಕ್ಕೆ ಮರುನಿರ್ದೇಶಿಸಲಾಗುತ್ತದೆ.", + "sync_restart_button": "ಮರುಪ್ರಾರಂಭ" } diff --git a/assets/l10n/app_ta.arb b/assets/l10n/app_ta.arb index e224d48cf..11b408f07 100644 --- a/assets/l10n/app_ta.arb +++ b/assets/l10n/app_ta.arb @@ -367,5 +367,7 @@ "update_officer_biometric": "அதிகாரியின் பயோமெட்ரிக் புதுப்பிப்பு", "submit_changes": "மாற்றங்களை சமர்ப்பிக்கவும்", "local_preferences_saved_msg": "உள்நாட்டு விருப்பங்கள் வெற்றிகரமாக சேமிக்கப்பட்டுள்ளன. மாற்றங்களைப் பார்க்க பயன்பாட்டை மறுதொடக்கம் செய்ய வேண்டும். நீங்கள் இப்போது வெளியேற விரும்புகிறீர்களா?", - "search_for_key": "விசையைத் தேடவும்" + "search_for_key": "விசையைத் தேடவும்", + "sync_restart_dialog_message": "மறுதொடக்கம் என்பதைக் கிளிக் செய்தவுடன் பயன்பாடு மறுதொடங்கும், மற்றும் நீங்கள் உள்நுழைவு பக்கத்திற்கு திருப்பிவிடப்படுவீர்கள்.", + "sync_restart_button": "மறுதொடக்கம்" } diff --git a/lib/main.dart b/lib/main.dart index db4d4d40a..77e8c109f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,15 +20,21 @@ import 'package:registration_client/provider/sync_provider.dart'; import 'package:registration_client/ui/login_page.dart'; import 'package:registration_client/utils/app_config.dart'; import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter/services.dart'; import 'package:registration_client/utils/inactivity_tracker.dart'; +import 'package:restart_app/restart_app.dart'; final GlobalKey rootNavigatorKey = GlobalKey(); final GlobalKey rootScaffoldMessengerKey = GlobalKey(); +/// MethodChannel name used by Android to request showing the sync-complete restart dialog. +const String _syncRestartChannel = 'io.mosip.registration_client/sync_restart'; + void main() async { enableFlutterDriverExtension(enableTextEntryEmulation: false); WidgetsFlutterBinding.ensureInitialized(); + _setupSyncRestartChannel(); final GlobalProvider appLanguage = GlobalProvider(); await FlutterConfig.loadEnvVariables(); await appLanguage.fetchLocale(); @@ -37,6 +43,44 @@ void main() async { ); } +void _setupSyncRestartChannel() { + const MethodChannel(_syncRestartChannel).setMethodCallHandler((MethodCall call) async { + if (call.method == 'showRestartDialog') { + // Ensure we have a valid context (e.g. after first frame when navigator is built) + void showRestartDialog() { + final context = rootNavigatorKey.currentContext; + if (context == null) return; + final loc = AppLocalizations.of(context); + if (loc == null) return; + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) => AlertDialog( + title: Text(loc.sync_completed_succesfully), + content: Text(loc.sync_restart_dialog_message), + actions: [ + FilledButton( + onPressed: () { + Navigator.of(dialogContext).pop(); + Restart.restartApp(); + }, + child: Text(loc.sync_restart_button), + ), + ], + ), + ); + } + final context = rootNavigatorKey.currentContext; + if (context != null) { + showRestartDialog(); + } else { + WidgetsBinding.instance.addPostFrameCallback((_) => showRestartDialog()); + } + } + return null; + }); +} + Future _handleAutoLogout() async { final ctx = rootNavigatorKey.currentContext; if (ctx == null) return; // Safety guard From 19616cd2d017469db3bf9d222ce034db35348e86 Mon Sep 17 00:00:00 2001 From: Madhuravas reddy Date: Mon, 23 Feb 2026 17:57:48 +0530 Subject: [PATCH 6/6] Resolved code rabbit review comments Signed-off-by: Madhuravas reddy --- lib/main.dart | 51 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 77e8c109f..70ce3b9aa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,15 +43,49 @@ void main() async { ); } +/// Max retries when showing the sync-restart dialog if context/localizations +/// are not ready yet (e.g. native call before widget tree is mounted). +const int _syncRestartDialogMaxRetries = 10; + void _setupSyncRestartChannel() { const MethodChannel(_syncRestartChannel).setMethodCallHandler((MethodCall call) async { if (call.method == 'showRestartDialog') { - // Ensure we have a valid context (e.g. after first frame when navigator is built) - void showRestartDialog() { + int attempt = 0; + + void tryShowRestartDialog() { + attempt++; final context = rootNavigatorKey.currentContext; - if (context == null) return; + if (context == null) { + if (attempt <= _syncRestartDialogMaxRetries) { + debugPrint( + 'SyncRestart: context not ready (attempt $attempt/$_syncRestartDialogMaxRetries), ' + 'scheduling retry on next frame.', + ); + WidgetsBinding.instance.addPostFrameCallback((_) => tryShowRestartDialog()); + } else { + debugPrint( + 'SyncRestart: WARNING — Restart prompt was not shown: navigator context was null ' + 'after $_syncRestartDialogMaxRetries attempts. User may need to restart the app manually.', + ); + } + return; + } final loc = AppLocalizations.of(context); - if (loc == null) return; + if (loc == null) { + if (attempt <= _syncRestartDialogMaxRetries) { + debugPrint( + 'SyncRestart: localizations not ready (attempt $attempt/$_syncRestartDialogMaxRetries), ' + 'scheduling retry on next frame.', + ); + WidgetsBinding.instance.addPostFrameCallback((_) => tryShowRestartDialog()); + } else { + debugPrint( + 'SyncRestart: WARNING — Restart prompt was not shown: AppLocalizations was null ' + 'after $_syncRestartDialogMaxRetries attempts. User may need to restart the app manually.', + ); + } + return; + } showDialog( context: context, barrierDismissible: false, @@ -70,11 +104,16 @@ void _setupSyncRestartChannel() { ), ); } + final context = rootNavigatorKey.currentContext; if (context != null) { - showRestartDialog(); + tryShowRestartDialog(); } else { - WidgetsBinding.instance.addPostFrameCallback((_) => showRestartDialog()); + debugPrint( + 'SyncRestart: native call arrived before widget tree ready, ' + 'scheduling restart dialog on next frame.', + ); + WidgetsBinding.instance.addPostFrameCallback((_) => tryShowRestartDialog()); } } return null;