Merge pull request #199 from vernu/dev

better handling of failed sms error messaging
This commit is contained in:
Israel Abebe
2026-03-11 11:45:17 +03:00
committed by GitHub
4 changed files with 120 additions and 21 deletions
@@ -199,9 +199,17 @@ public class TextBeeUtils {
Log.d(TAG, "Could not get SIM slot index for subscription " + subscriptionInfo.getSubscriptionId());
}
// Get MCC
// Get MCC (getMccString() is API 29+; use getMcc() on older devices)
try {
String mcc = subscriptionInfo.getMccString();
String mcc = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mcc = subscriptionInfo.getMccString();
} else {
int mccInt = subscriptionInfo.getMcc();
if (mccInt != Integer.MAX_VALUE) {
mcc = String.format("%03d", mccInt);
}
}
if (mcc != null && !mcc.isEmpty()) {
simInfo.setMcc(mcc);
}
@@ -209,9 +217,17 @@ public class TextBeeUtils {
Log.d(TAG, "Could not get MCC for subscription " + subscriptionInfo.getSubscriptionId());
}
// Get MNC
// Get MNC (getMncString() is API 29+; use getMnc() on older devices)
try {
String mnc = subscriptionInfo.getMncString();
String mnc = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mnc = subscriptionInfo.getMncString();
} else {
int mncInt = subscriptionInfo.getMnc();
if (mncInt != Integer.MAX_VALUE) {
mnc = String.valueOf(mncInt);
}
}
if (mnc != null && !mnc.isEmpty()) {
simInfo.setMnc(mnc);
}
@@ -7,6 +7,9 @@ import android.content.Intent;
import android.telephony.SmsManager;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import com.vernu.sms.AppConstants;
import com.vernu.sms.dtos.SMSDTO;
import com.vernu.sms.helpers.SharedPreferenceHelper;
@@ -19,6 +22,29 @@ public class SMSStatusReceiver extends BroadcastReceiver {
public static final String SMS_SENT = "SMS_SENT";
public static final String SMS_DELIVERED = "SMS_DELIVERED";
/**
* Resolves a result code to the constant name (e.g. SmsManager.RESULT_ERROR_GENERIC_FAILURE)
* via reflection. Returns null if no matching constant is found.
*/
private static String getResultCodeName(int resultCode) {
for (Class<?> clazz : new Class<?>[]{ SmsManager.class, Activity.class }) {
try {
for (Field field : clazz.getDeclaredFields()) {
if (field.getType() != int.class) continue;
if (!Modifier.isStatic(field.getModifiers()) || !Modifier.isFinal(field.getModifiers())) continue;
if (!field.getName().startsWith("RESULT_")) continue;
field.setAccessible(true);
if (field.getInt(null) == resultCode) {
return clazz.getSimpleName() + "." + field.getName();
}
}
} catch (Exception e) {
Log.w(TAG, "Reflection failed for " + clazz.getSimpleName() + ": " + e.getMessage());
}
}
return null;
}
@Override
public void onReceive(Context context, Intent intent) {
String smsId = intent.getStringExtra("sms_id");
@@ -30,13 +56,13 @@ public class SMSStatusReceiver extends BroadcastReceiver {
smsDTO.setSmsBatchId(smsBatchId);
if (SMS_SENT.equals(action)) {
handleSentStatus(context, getResultCode(), smsDTO);
handleSentStatus(context, intent, getResultCode(), smsDTO);
} else if (SMS_DELIVERED.equals(action)) {
handleDeliveredStatus(context, getResultCode(), smsDTO);
}
}
private void handleSentStatus(Context context, int resultCode, SMSDTO smsDTO) {
private void handleSentStatus(Context context, Intent intent, int resultCode, SMSDTO smsDTO) {
long timestamp = System.currentTimeMillis();
String errorMessage = "";
@@ -47,7 +73,11 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.d(TAG, "SMS sent successfully - ID: " + smsDTO.getSmsId());
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
errorMessage = "Generic failure";
errorMessage = "SMS failed on device. Common causes: no SMS credit on SIM, weak signal, or carrier blocked. Check SIM balance and signal, then try again.";
int radioCode = intent.getIntExtra("errorCode", -1);
if (radioCode != -1) {
errorMessage += " (code " + radioCode + ")";
}
smsDTO.setStatus("FAILED");
smsDTO.setFailedAtInMillis(timestamp);
smsDTO.setErrorCode(String.valueOf(resultCode));
@@ -55,7 +85,7 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
errorMessage = "Radio off";
errorMessage = "Mobile radio is off (e.g. airplane mode). Turn off airplane mode and ensure cellular is on.";
smsDTO.setStatus("FAILED");
smsDTO.setFailedAtInMillis(timestamp);
smsDTO.setErrorCode(String.valueOf(resultCode));
@@ -63,7 +93,7 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
break;
case SmsManager.RESULT_ERROR_NULL_PDU:
errorMessage = "Null PDU";
errorMessage = "Message could not be sent; invalid format or carrier issue. Try a shorter message or different recipient.";
smsDTO.setStatus("FAILED");
smsDTO.setFailedAtInMillis(timestamp);
smsDTO.setErrorCode(String.valueOf(resultCode));
@@ -71,7 +101,7 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
break;
case SmsManager.RESULT_ERROR_NO_SERVICE:
errorMessage = "No service";
errorMessage = "No cellular service. Check signal and try again when you have coverage.";
smsDTO.setStatus("FAILED");
smsDTO.setFailedAtInMillis(timestamp);
smsDTO.setErrorCode(String.valueOf(resultCode));
@@ -79,7 +109,7 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
break;
case SmsManager.RESULT_ERROR_LIMIT_EXCEEDED:
errorMessage = "Sending limit exceeded";
errorMessage = "Device/carrier send limit reached (too many SMS in a short time). Wait a few minutes or lower the send rate.";
smsDTO.setStatus("FAILED");
smsDTO.setFailedAtInMillis(timestamp);
smsDTO.setErrorCode(String.valueOf(resultCode));
@@ -87,7 +117,7 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
break;
case SmsManager.RESULT_ERROR_SHORT_CODE_NOT_ALLOWED:
errorMessage = "Short code not allowed";
errorMessage = "Short code not allowed on this carrier. Use a full phone number.";
smsDTO.setStatus("FAILED");
smsDTO.setFailedAtInMillis(timestamp);
smsDTO.setErrorCode(String.valueOf(resultCode));
@@ -95,7 +125,7 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
break;
case SmsManager.RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED:
errorMessage = "Short code never allowed";
errorMessage = "Short codes are not supported on this carrier. Use a full phone number.";
smsDTO.setStatus("FAILED");
smsDTO.setFailedAtInMillis(timestamp);
smsDTO.setErrorCode(String.valueOf(resultCode));
@@ -103,7 +133,7 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
break;
case SmsManager.RESULT_NETWORK_ERROR:
errorMessage = "Network error";
errorMessage = "Network error while sending. Check signal and try again.";
smsDTO.setStatus("FAILED");
smsDTO.setFailedAtInMillis(timestamp);
smsDTO.setErrorCode(String.valueOf(resultCode));
@@ -111,12 +141,13 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
break;
default:
errorMessage = "Unknown error";
String codeName = getResultCodeName(resultCode);
errorMessage = codeName != null ? codeName : ("Unknown error (code " + resultCode + ")");
smsDTO.setStatus("FAILED");
smsDTO.setFailedAtInMillis(timestamp);
smsDTO.setErrorCode(String.valueOf(resultCode));
smsDTO.setErrorMessage(errorMessage);
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Unknown error code: " + resultCode);
Log.e(TAG, "SMS failed to send - ID: " + smsDTO.getSmsId() + ", Error: " + errorMessage);
break;
}
@@ -134,18 +165,19 @@ public class SMSStatusReceiver extends BroadcastReceiver {
Log.d(TAG, "SMS delivered successfully - ID: " + smsDTO.getSmsId());
break;
case Activity.RESULT_CANCELED:
errorMessage = "Delivery canceled";
errorMessage = "Delivery report was canceled (e.g. carrier does not support delivery receipts). Message may still have been delivered.";
smsDTO.setStatus("DELIVERY_FAILED");
smsDTO.setErrorCode(String.valueOf(resultCode));
smsDTO.setErrorMessage(errorMessage);
Log.e(TAG, "SMS delivery failed - ID: " + smsDTO.getSmsId() + ", Error code: " + resultCode + ", Error: " + errorMessage);
break;
default:
errorMessage = "Unknown delivery error";
String deliveryCodeName = getResultCodeName(resultCode);
errorMessage = deliveryCodeName != null ? deliveryCodeName : ("Unknown delivery error (code " + resultCode + ")");
smsDTO.setStatus("DELIVERY_FAILED");
smsDTO.setErrorCode(String.valueOf(resultCode));
smsDTO.setErrorMessage(errorMessage);
Log.e(TAG, "SMS delivery failed - ID: " + smsDTO.getSmsId() + ", Unknown error code: " + resultCode);
Log.e(TAG, "SMS delivery failed - ID: " + smsDTO.getSmsId() + ", Error: " + errorMessage);
break;
}
@@ -1,5 +1,6 @@
package com.vernu.sms.services;
import android.app.ForegroundServiceStartNotAllowedException;
import android.app.*;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -42,8 +43,14 @@ public class StickyNotificationService extends Service {
if (stickyNotificationEnabled) {
Notification notification = createNotification();
startForeground(1, notification);
Log.i(TAG, "Started foreground service with sticky notification");
try {
startForeground(1, notification);
Log.i(TAG, "Started foreground service with sticky notification");
} catch (ForegroundServiceStartNotAllowedException e) {
Log.w(TAG, "Cannot start foreground from background, stopping service: " + e.getMessage());
stopSelf();
return;
}
} else {
Log.i(TAG, "Sticky notification disabled by user preference");
}
@@ -48,6 +48,30 @@ export class SmsQueueProcessor {
`SMS Job ${job.id} completed, success: ${response.successCount}, failures: ${response.failureCount}`,
)
// Mark individual SMS records as failed when their FCM push failed
for (let i = 0; i < response.responses.length; i++) {
if (!response.responses[i].success) {
try {
const smsData = JSON.parse(fcmMessages[i].data.smsData)
await this.smsModel.findByIdAndUpdate(smsData.smsId, {
$set: {
status: 'failed',
failedAt: new Date(),
errorCode: 'FCM_DELIVERY_FAILED',
errorMessage:
response.responses[i].error?.message ||
'FCM push notification delivery failed',
},
})
} catch (parseError) {
this.logger.error(
`Failed to mark SMS as failed for FCM message index ${i}`,
parseError,
)
}
}
}
// Update device SMS count
await this.deviceModel
.findByIdAndUpdate(deviceId, {
@@ -77,6 +101,26 @@ export class SmsQueueProcessor {
} catch (error) {
this.logger.error(`Failed to process SMS job ${job.id}`, error)
// Mark all individual SMS in this batch of FCM messages as failed
for (const fcmMessage of fcmMessages) {
try {
const smsData = JSON.parse(fcmMessage.data.smsData)
await this.smsModel.findByIdAndUpdate(smsData.smsId, {
$set: {
status: 'failed',
failedAt: new Date(),
errorCode: 'FCM_SEND_ERROR',
errorMessage: error?.message || 'FCM sendEach call failed',
},
})
} catch (parseError) {
this.logger.error(
'Failed to mark SMS as failed after FCM error',
parseError,
)
}
}
const smsBatch = await this.smsBatchModel.findByIdAndUpdate(
smsBatchId,
{