From 141b8b334a96a4ad23714e5a8fc7a2290aa112e6 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 26 Jan 2026 14:17:08 +0300 Subject: [PATCH] fix: handle duplicate received sms issue --- .../main/java/com/vernu/sms/dtos/SMSDTO.java | 9 +++ .../sms/receivers/SMSBroadcastReceiver.java | 76 +++++++++++++++++++ .../sms/receivers/SMSStatusReceiver.java | 8 ++ .../vernu/sms/workers/SMSReceivedWorker.java | 15 +++- api/src/gateway/gateway.service.ts | 23 ++++++ 5 files changed, 128 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/vernu/sms/dtos/SMSDTO.java b/android/app/src/main/java/com/vernu/sms/dtos/SMSDTO.java index 20a1867..9f17000 100644 --- a/android/app/src/main/java/com/vernu/sms/dtos/SMSDTO.java +++ b/android/app/src/main/java/com/vernu/sms/dtos/SMSDTO.java @@ -6,6 +6,7 @@ public class SMSDTO { private String sender; private String message = ""; private long receivedAtInMillis; + private String fingerprint; private String smsId; private String smsBatchId; @@ -107,4 +108,12 @@ public class SMSDTO { public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + + public String getFingerprint() { + return fingerprint; + } + + public void setFingerprint(String fingerprint) { + this.fingerprint = fingerprint; + } } diff --git a/android/app/src/main/java/com/vernu/sms/receivers/SMSBroadcastReceiver.java b/android/app/src/main/java/com/vernu/sms/receivers/SMSBroadcastReceiver.java index 80eda09..f3c0cb9 100644 --- a/android/app/src/main/java/com/vernu/sms/receivers/SMSBroadcastReceiver.java +++ b/android/app/src/main/java/com/vernu/sms/receivers/SMSBroadcastReceiver.java @@ -11,11 +11,18 @@ import com.vernu.sms.dtos.SMSDTO; import com.vernu.sms.helpers.SharedPreferenceHelper; import com.vernu.sms.workers.SMSReceivedWorker; +import java.security.MessageDigest; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class SMSBroadcastReceiver extends BroadcastReceiver { private static final String TAG = "SMSBroadcastReceiver"; + // In-memory cache to prevent rapid duplicate processing (5 seconds TTL) + private static final ConcurrentHashMap processedFingerprints = new ConcurrentHashMap<>(); + private static final long CACHE_TTL_MS = 5000; // 5 seconds @Override public void onReceive(Context context, Intent intent) { @@ -60,6 +67,29 @@ public class SMSBroadcastReceiver extends BroadcastReceiver { // receivedSMSDTO.setMessage(receivedSMS.getMessage()); // receivedSMSDTO.setReceivedAt(receivedSMS.getReceivedAt()); + // Generate fingerprint for deduplication + String fingerprint = generateFingerprint( + receivedSMSDTO.getSender(), + receivedSMSDTO.getMessage(), + receivedSMSDTO.getReceivedAtInMillis() + ); + receivedSMSDTO.setFingerprint(fingerprint); + + // Check in-memory cache to prevent rapid duplicate processing + long currentTime = System.currentTimeMillis(); + Long lastProcessedTime = processedFingerprints.get(fingerprint); + + if (lastProcessedTime != null && (currentTime - lastProcessedTime) < CACHE_TTL_MS) { + Log.d(TAG, "Duplicate SMS detected in cache, skipping: " + fingerprint); + return; + } + + // Update cache + processedFingerprints.put(fingerprint, currentTime); + + // Clean up old cache entries periodically + cleanupCache(currentTime); + SMSReceivedWorker.enqueueWork(context, deviceId, apiKey, receivedSMSDTO); } @@ -69,4 +99,50 @@ public class SMSBroadcastReceiver extends BroadcastReceiver { // appDatabase.localReceivedSMSDao().insertAll(localReceivedSMS); // }); // } + + /** + * Generate a unique fingerprint for an SMS message based on sender, message content, and timestamp + */ + private String generateFingerprint(String sender, String message, long timestamp) { + try { + String data = (sender != null ? sender : "") + "|" + + (message != null ? message : "") + "|" + + timestamp; + + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] hashBytes = md.digest(data.getBytes("UTF-8")); + + StringBuilder sb = new StringBuilder(); + for (byte b : hashBytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (Exception e) { + Log.e(TAG, "Error generating fingerprint: " + e.getMessage()); + // Fallback to simple string concatenation if MD5 fails + return (sender != null ? sender : "") + "_" + + (message != null ? message : "") + "_" + + timestamp; + } + } + + /** + * Clean up old cache entries to prevent memory leaks + */ + private void cleanupCache(long currentTime) { + // Only cleanup occasionally (every 100 entries processed) + if (processedFingerprints.size() > 100) { + Set keysToRemove = new HashSet<>(); + for (String key : processedFingerprints.keySet()) { + Long timestamp = processedFingerprints.get(key); + if (timestamp != null && (currentTime - timestamp) > CACHE_TTL_MS) { + keysToRemove.add(key); + } + } + for (String key : keysToRemove) { + processedFingerprints.remove(key); + } + Log.d(TAG, "Cleaned up " + keysToRemove.size() + " expired cache entries"); + } + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/vernu/sms/receivers/SMSStatusReceiver.java b/android/app/src/main/java/com/vernu/sms/receivers/SMSStatusReceiver.java index e50e7a4..0cafb3d 100644 --- a/android/app/src/main/java/com/vernu/sms/receivers/SMSStatusReceiver.java +++ b/android/app/src/main/java/com/vernu/sms/receivers/SMSStatusReceiver.java @@ -102,6 +102,14 @@ public class SMSStatusReceiver extends BroadcastReceiver { smsDTO.setErrorMessage(errorMessage); Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage); break; + case SmsManager.RESULT_NETWORK_ERROR: + errorMessage = "Network error"; + smsDTO.setStatus("FAILED"); + smsDTO.setFailedAtInMillis(timestamp); + smsDTO.setErrorCode(String.valueOf(resultCode)); + smsDTO.setErrorMessage(errorMessage); + Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage); + break; default: errorMessage = "Unknown error"; smsDTO.setStatus("FAILED"); diff --git a/android/app/src/main/java/com/vernu/sms/workers/SMSReceivedWorker.java b/android/app/src/main/java/com/vernu/sms/workers/SMSReceivedWorker.java index 51c8c38..1f45182 100644 --- a/android/app/src/main/java/com/vernu/sms/workers/SMSReceivedWorker.java +++ b/android/app/src/main/java/com/vernu/sms/workers/SMSReceivedWorker.java @@ -94,13 +94,22 @@ public class SMSReceivedWorker extends Worker { .addTag("sms_received") .build(); - String uniqueWorkName = "sms_received_" + System.currentTimeMillis(); + // Use fingerprint for unique work name if available, otherwise fallback to timestamp + String uniqueWorkName; + if (smsDTO.getFingerprint() != null && !smsDTO.getFingerprint().isEmpty()) { + uniqueWorkName = "sms_received_" + smsDTO.getFingerprint(); + } else { + // Fallback to timestamp if fingerprint is not available + uniqueWorkName = "sms_received_" + System.currentTimeMillis(); + Log.w(TAG, "Fingerprint not available, using timestamp for work name"); + } + WorkManager.getInstance(context) .beginUniqueWork(uniqueWorkName, - androidx.work.ExistingWorkPolicy.APPEND_OR_REPLACE, + androidx.work.ExistingWorkPolicy.KEEP, workRequest) .enqueue(); - Log.d(TAG, "Work enqueued for received SMS from: " + smsDTO.getSender()); + Log.d(TAG, "Work enqueued for received SMS from: " + smsDTO.getSender() + " with fingerprint: " + uniqueWorkName); } } \ No newline at end of file diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts index ec69c33..1f26ed5 100644 --- a/api/src/gateway/gateway.service.ts +++ b/api/src/gateway/gateway.service.ts @@ -545,6 +545,29 @@ recipient, ? new Date(dto.receivedAtInMillis) : dto.receivedAt + // Deduplication: Check for existing SMS with same device, sender, message, and receivedAt (within ±5 seconds tolerance) + const toleranceMs = 5000 // 5 seconds + const toleranceStart = new Date(receivedAt.getTime() - toleranceMs) + const toleranceEnd = new Date(receivedAt.getTime() + toleranceMs) + + const existingSMS = await this.smsModel.findOne({ + device: device._id, + type: SMSType.RECEIVED, + sender: dto.sender, + message: dto.message, + receivedAt: { + $gte: toleranceStart, + $lte: toleranceEnd, + }, + }) + + if (existingSMS) { + console.log( + `Duplicate SMS detected for device ${deviceId}, sender ${dto.sender}, returning existing record: ${existingSMS._id}`, + ) + return existingSMS + } + const sms = await this.smsModel.create({ device: device._id, message: dto.message,