fix: handle duplicate received sms issue

This commit is contained in:
isra el
2026-01-26 14:17:08 +03:00
parent f9fed4d3d8
commit 141b8b334a
5 changed files with 128 additions and 3 deletions

View File

@@ -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;
}
}

View File

@@ -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<String, Long> 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<String> 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");
}
}
}

View File

@@ -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");

View File

@@ -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);
}
}

View File

@@ -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,