Skip to content

Commit 17351e2

Browse files
authored
Merge pull request #198 from vernu/dev
add sms sending delay and retry config
2 parents e14b71a + 8eec17b commit 17351e2

File tree

11 files changed

+284
-108
lines changed

11 files changed

+284
-108
lines changed

android/app/src/main/java/com/vernu/sms/AppConstants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ public class AppConstants {
2424
public static final String SHARED_PREFS_HEARTBEAT_INTERVAL_MINUTES_KEY = "HEARTBEAT_INTERVAL_MINUTES";
2525
public static final String SHARED_PREFS_SMS_FILTER_CONFIG_KEY = "SMS_FILTER_CONFIG";
2626
public static final String SHARED_PREFS_DEVICE_NAME_KEY = "DEVICE_NAME";
27+
public static final String SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY = "SMS_SEND_DELAY_SECONDS";
28+
/** Default delay between SMS sends (seconds). 5s helps avoid carrier/device throttling. */
29+
public static final int DEFAULT_SMS_SEND_DELAY_SECONDS = 5;
2730
}

android/app/src/main/java/com/vernu/sms/activities/MainActivity.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
import com.vernu.sms.activities.SMSFilterActivity;
1111
import android.os.Build;
1212
import android.os.Bundle;
13+
import android.os.Handler;
14+
import android.os.Looper;
15+
import android.text.Editable;
16+
import android.text.TextWatcher;
1317
import android.util.Log;
1418
import android.view.View;
1519
import android.widget.Button;
@@ -49,13 +53,16 @@ public class MainActivity extends AppCompatActivity {
4953

5054
private Context mContext;
5155
private Switch gatewaySwitch, receiveSMSSwitch, stickyNotificationSwitch;
52-
private EditText apiKeyEditText, fcmTokenEditText, deviceIdEditText, deviceNameEditText;
56+
private EditText apiKeyEditText, fcmTokenEditText, deviceIdEditText, deviceNameEditText, smsSendDelayEditText;
5357
private Button registerDeviceBtn, grantSMSPermissionBtn, scanQRBtn, checkUpdatesBtn, configureFilterBtn;
5458
private ImageButton copyDeviceIdImgBtn;
5559
private TextView deviceBrandAndModelTxt, deviceIdTxt, appVersionNameTxt, appVersionCodeTxt;
5660
private RadioGroup defaultSimSlotRadioGroup;
5761
private static final int SCAN_QR_REQUEST_CODE = 49374;
5862
private static final int PERMISSION_REQUEST_CODE = 0;
63+
private static final long SMS_DELAY_SAVE_DEBOUNCE_MS = 3000L;
64+
private final Handler smsDelaySaveHandler = new Handler(Looper.getMainLooper());
65+
private Runnable smsDelaySaveRunnable;
5966
private String deviceId = null;
6067
private static final String TAG = "MainActivity";
6168

@@ -84,6 +91,7 @@ protected void onCreate(Bundle savedInstanceState) {
8491
appVersionCodeTxt = findViewById(R.id.appVersionCodeTxt);
8592
checkUpdatesBtn = findViewById(R.id.checkUpdatesBtn);
8693
configureFilterBtn = findViewById(R.id.configureFilterBtn);
94+
smsSendDelayEditText = findViewById(R.id.smsSendDelayEditText);
8795

8896
deviceIdTxt.setText(deviceId);
8997
deviceIdEditText.setText(deviceId);
@@ -255,6 +263,63 @@ public void onFailure(Call<RegisterDeviceResponseDTO> call, Throwable t) {
255263
Intent filterIntent = new Intent(MainActivity.this, SMSFilterActivity.class);
256264
startActivity(filterIntent);
257265
});
266+
267+
// SMS Send Delay setting: save 3 seconds after user stops typing
268+
int currentDelay = SharedPreferenceHelper.getSharedPreferenceInt(
269+
mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, AppConstants.DEFAULT_SMS_SEND_DELAY_SECONDS);
270+
smsSendDelayEditText.setText(String.valueOf(currentDelay));
271+
smsDelaySaveRunnable = this::saveSendDelay;
272+
smsSendDelayEditText.addTextChangedListener(new TextWatcher() {
273+
@Override
274+
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
275+
276+
@Override
277+
public void onTextChanged(CharSequence s, int start, int before, int count) {}
278+
279+
@Override
280+
public void afterTextChanged(Editable s) {
281+
smsDelaySaveHandler.removeCallbacks(smsDelaySaveRunnable);
282+
smsDelaySaveHandler.postDelayed(smsDelaySaveRunnable, SMS_DELAY_SAVE_DEBOUNCE_MS);
283+
}
284+
});
285+
smsSendDelayEditText.setOnEditorActionListener((v, actionId, event) -> {
286+
smsDelaySaveHandler.removeCallbacks(smsDelaySaveRunnable);
287+
saveSendDelay();
288+
return false;
289+
});
290+
}
291+
292+
private void saveSendDelay() {
293+
String text = smsSendDelayEditText.getText().toString().trim();
294+
if (text.isEmpty()) {
295+
int defaultDelay = AppConstants.DEFAULT_SMS_SEND_DELAY_SECONDS;
296+
smsSendDelayEditText.setText(String.valueOf(defaultDelay));
297+
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, defaultDelay);
298+
Snackbar.make(smsSendDelayEditText, "SMS send delay saved (" + defaultDelay + " sec)", Snackbar.LENGTH_SHORT).show();
299+
return;
300+
}
301+
try {
302+
int value = Integer.parseInt(text);
303+
if (value < 0) {
304+
value = 0;
305+
smsSendDelayEditText.setText("0");
306+
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, 0);
307+
Snackbar.make(smsSendDelayEditText, "Minimum delay is 0 seconds. Saved.", Snackbar.LENGTH_SHORT).show();
308+
} else if (value > 3600) {
309+
value = 3600;
310+
smsSendDelayEditText.setText("3600");
311+
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, 3600);
312+
Snackbar.make(smsSendDelayEditText, "Maximum delay is 3600 seconds. Saved.", Snackbar.LENGTH_SHORT).show();
313+
} else {
314+
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, value);
315+
Snackbar.make(smsSendDelayEditText, "SMS send delay saved (" + value + " sec)", Snackbar.LENGTH_SHORT).show();
316+
}
317+
} catch (NumberFormatException e) {
318+
int defaultDelay = AppConstants.DEFAULT_SMS_SEND_DELAY_SECONDS;
319+
smsSendDelayEditText.setText(String.valueOf(defaultDelay));
320+
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY, defaultDelay);
321+
Snackbar.make(smsSendDelayEditText, "Invalid value. Reset to " + defaultDelay + " sec.", Snackbar.LENGTH_SHORT).show();
322+
}
258323
}
259324

260325
private void renderAvailableSimOptions() {

android/app/src/main/java/com/vernu/sms/dtos/HeartbeatInputDTO.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class HeartbeatInputDTO {
1616
private String timezone;
1717
private String locale;
1818
private Boolean receiveSMSEnabled;
19+
private Integer smsSendDelaySeconds;
1920
private SimInfoCollectionDTO simInfo;
2021

2122
public HeartbeatInputDTO() {
@@ -141,6 +142,14 @@ public void setReceiveSMSEnabled(Boolean receiveSMSEnabled) {
141142
this.receiveSMSEnabled = receiveSMSEnabled;
142143
}
143144

145+
public Integer getSmsSendDelaySeconds() {
146+
return smsSendDelaySeconds;
147+
}
148+
149+
public void setSmsSendDelaySeconds(Integer smsSendDelaySeconds) {
150+
this.smsSendDelaySeconds = smsSendDelaySeconds;
151+
}
152+
144153
public SimInfoCollectionDTO getSimInfo() {
145154
return simInfo;
146155
}

android/app/src/main/java/com/vernu/sms/helpers/HeartbeatHelper.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,14 @@ public static boolean sendHeartbeat(Context context, String deviceId, String api
138138
);
139139
heartbeatInput.setReceiveSMSEnabled(receiveSMSEnabled);
140140

141+
// SMS send delay (device queue)
142+
int smsSendDelaySeconds = SharedPreferenceHelper.getSharedPreferenceInt(
143+
context,
144+
AppConstants.SHARED_PREFS_SMS_SEND_DELAY_SECONDS_KEY,
145+
AppConstants.DEFAULT_SMS_SEND_DELAY_SECONDS
146+
);
147+
heartbeatInput.setSmsSendDelaySeconds(smsSendDelaySeconds);
148+
141149
// Collect SIM information
142150
SimInfoCollectionDTO simInfoCollection = new SimInfoCollectionDTO();
143151
simInfoCollection.setLastUpdated(System.currentTimeMillis());

android/app/src/main/java/com/vernu/sms/helpers/SMSHelper.java

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,14 @@
99
import android.telephony.SubscriptionManager;
1010
import android.util.Log;
1111

12-
import com.vernu.sms.ApiManager;
1312
import com.vernu.sms.AppConstants;
1413
import com.vernu.sms.TextBeeUtils;
1514
import com.vernu.sms.dtos.SMSDTO;
16-
import com.vernu.sms.dtos.SMSForwardResponseDTO;
1715
import com.vernu.sms.receivers.SMSStatusReceiver;
18-
import com.vernu.sms.services.GatewayApiService;
16+
import com.vernu.sms.workers.SMSStatusUpdateWorker;
1917

2018
import java.util.ArrayList;
2119

22-
import retrofit2.Call;
23-
import retrofit2.Callback;
24-
import retrofit2.Response;
25-
2620
public class SMSHelper {
2721
private static final String TAG = "SMSHelper";
2822

@@ -179,25 +173,8 @@ private static void updateSMSStatus(Context context, SMSDTO smsDTO) {
179173
Log.e(TAG, "Device ID or API key not found");
180174
return;
181175
}
182-
183-
GatewayApiService apiService = ApiManager.getApiService();
184-
Call<SMSForwardResponseDTO> call = apiService.updateSMSStatus(deviceId, apiKey, smsDTO);
185-
186-
call.enqueue(new Callback<SMSForwardResponseDTO>() {
187-
@Override
188-
public void onResponse(Call<SMSForwardResponseDTO> call, Response<SMSForwardResponseDTO> response) {
189-
if (response.isSuccessful()) {
190-
Log.d(TAG, "SMS status updated successfully - ID: " + smsDTO.getSmsId() + ", Status: " + smsDTO.getStatus());
191-
} else {
192-
Log.e(TAG, "Failed to update SMS status. Response code: " + response.code());
193-
}
194-
}
195-
196-
@Override
197-
public void onFailure(Call<SMSForwardResponseDTO> call, Throwable t) {
198-
Log.e(TAG, "API call failed: " + t.getMessage());
199-
}
200-
});
176+
177+
SMSStatusUpdateWorker.enqueueWork(context, deviceId, apiKey, smsDTO);
201178
}
202179

203180
private static PendingIntent createSentPendingIntent(Context context, String smsId, String smsBatchId) {

android/app/src/main/java/com/vernu/sms/services/FCMService.java

Lines changed: 9 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616
import com.vernu.sms.AppConstants;
1717
import com.vernu.sms.R;
1818
import com.vernu.sms.activities.MainActivity;
19-
import com.vernu.sms.helpers.SMSHelper;
2019
import com.vernu.sms.helpers.SharedPreferenceHelper;
2120
import com.vernu.sms.helpers.HeartbeatHelper;
2221
import com.vernu.sms.helpers.HeartbeatManager;
2322
import com.vernu.sms.models.SMSPayload;
24-
import com.vernu.sms.TextBeeUtils;
23+
import com.vernu.sms.workers.SmsSendWorker;
2524
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
2625
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
2726
import com.vernu.sms.ApiManager;
@@ -106,99 +105,28 @@ private void handleHeartbeatCheck() {
106105
}
107106

108107
/**
109-
* Send SMS to recipients using the provided payload
108+
* Enqueue SMS to recipients via the device-side send queue.
109+
* SIM resolution and rate limiting are handled by SmsSendWorker.
110110
*/
111111
private void sendSMS(SMSPayload smsPayload) {
112112
if (smsPayload == null) {
113113
Log.e(TAG, "SMS payload is null");
114114
return;
115115
}
116116

117-
// Determine which SIM to use (priority: backend-provided > app preference > device default)
118-
Integer simSubscriptionId = null;
119-
120-
// First, check if backend provided a SIM subscription ID
121-
if (smsPayload.getSimSubscriptionId() != null) {
122-
int backendSimId = smsPayload.getSimSubscriptionId();
123-
// Validate that the subscription ID exists
124-
if (TextBeeUtils.isValidSubscriptionId(this, backendSimId)) {
125-
simSubscriptionId = backendSimId;
126-
Log.d(TAG, "Using backend-provided SIM subscription ID: " + backendSimId);
127-
} else {
128-
Log.w(TAG, "Backend-provided SIM subscription ID " + backendSimId + " is not valid, falling back to app preference");
129-
}
130-
}
131-
132-
// If backend didn't provide a valid SIM, check app preference
133-
if (simSubscriptionId == null) {
134-
int preferredSim = SharedPreferenceHelper.getSharedPreferenceInt(
135-
this, AppConstants.SHARED_PREFS_PREFERRED_SIM_KEY, -1);
136-
if (preferredSim != -1) {
137-
// Validate that the preferred SIM still exists
138-
if (TextBeeUtils.isValidSubscriptionId(this, preferredSim)) {
139-
simSubscriptionId = preferredSim;
140-
Log.d(TAG, "Using app-preferred SIM subscription ID: " + preferredSim);
141-
} else {
142-
Log.w(TAG, "App-preferred SIM subscription ID " + preferredSim + " is no longer valid, using device default");
143-
}
144-
}
145-
}
146-
147-
// Check if SMS payload contains valid recipients
148117
String[] recipients = smsPayload.getRecipients();
149118
if (recipients == null || recipients.length == 0) {
150119
Log.e(TAG, "No recipients found in SMS payload");
151120
return;
152121
}
153-
154-
// Send SMS to each recipient
155-
boolean atLeastOneSent = false;
156-
int sentCount = 0;
157-
int failedCount = 0;
158-
122+
159123
for (String recipient : recipients) {
160-
boolean smsSent;
161-
162-
// Send using determined SIM (or device default if simSubscriptionId is null)
163-
if (simSubscriptionId == null) {
164-
// Use default SIM
165-
Log.d(TAG, "Using device default SIM");
166-
smsSent = SMSHelper.sendSMS(
167-
recipient,
168-
smsPayload.getMessage(),
169-
smsPayload.getSmsId(),
170-
smsPayload.getSmsBatchId(),
171-
this
172-
);
173-
} else {
174-
// Use specific SIM
175-
try {
176-
smsSent = SMSHelper.sendSMSFromSpecificSim(
177-
recipient,
178-
smsPayload.getMessage(),
179-
simSubscriptionId,
180-
smsPayload.getSmsId(),
181-
smsPayload.getSmsBatchId(),
182-
this
183-
);
184-
} catch (Exception e) {
185-
Log.e(TAG, "Error sending SMS from specific SIM: " + e.getMessage());
186-
smsSent = false;
187-
}
188-
}
189-
190-
// Track sent and failed counts
191-
if (smsSent) {
192-
sentCount++;
193-
atLeastOneSent = true;
194-
} else {
195-
failedCount++;
196-
}
124+
SmsSendWorker.enqueue(this, recipient, smsPayload.getMessage(),
125+
smsPayload.getSmsId(), smsPayload.getSmsBatchId(),
126+
smsPayload.getSimSubscriptionId());
197127
}
198-
199-
// Log summary
200-
Log.d(TAG, "SMS sending complete - Batch: " + smsPayload.getSmsBatchId() +
201-
", Sent: " + sentCount + ", Failed: " + failedCount);
128+
129+
Log.d(TAG, "Enqueued " + recipients.length + " SMS for sending - Batch: " + smsPayload.getSmsBatchId());
202130
}
203131

204132
@Override

0 commit comments

Comments
 (0)